mathieuprog / polymorphic_embed Goto Github PK
View Code? Open in Web Editor NEWPolymorphic embeds in Ecto
License: Apache License 2.0
Polymorphic embeds in Ecto
License: Apache License 2.0
First of all, thanks for the amazing work on this library! 😄
I think I stepped into a tiny bug when it comes to the types
function, which I'm heavily relying upon for some reflection into some generated code of my own.
Given this module content (all in one file just for the sake of reproduction):
defmodule MyApp.Channel.Email do
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field :address, :string
field :confirmed, :boolean
end
def changeset(email, params) do
email
|> cast(params, ~w(address confirmed)a)
|> validate_required(:address)
|> validate_length(:address, min: 4)
end
end
defmodule MyApp.Channel.SMS do
use Ecto.Schema
@primary_key false
embedded_schema do
field :number, :string
end
end
defmodule PolymorphicEmbedBug do
use Ecto.Schema
import Ecto.Changeset
import PolymorphicEmbed
schema "reminders" do
field :date, :utc_datetime
field :text, :string
polymorphic_embeds_one :channel,
types: [
sms: MyApp.Channel.SMS,
email: MyApp.Channel.Email
],
on_type_not_found: :raise,
on_replace: :update
end
def changeset(struct, values) do
struct
|> cast(values, [:date, :text])
|> cast_polymorphic_embed(:channel, required: true)
|> validate_required(:date)
end
end
I get this behavior when I run iex
for the first time, having the module being compiled:
9:16:02 › iex -S mix
Erlang/OTP 25 [erts-13.0.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
==> decimal
Compiling 4 files (.ex)
Generated decimal app
===> Analyzing applications...
===> Compiling telemetry
==> jason
Compiling 10 files (.ex)
Generated jason app
==> ecto
Compiling 56 files (.ex)
Generated ecto app
==> polymorphic_embed
Compiling 2 files (.ex)
Generated polymorphic_embed app
==> polymorphic_embed_bug
Compiling 1 file (.ex)
Generated polymorphic_embed_bug app
Interactive Elixir (1.13.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> PolymorphicEmbed.types(PolymorphicEmbedBug, :channel)
[:sms, :email]
But if I just stop iex
(hence the BEAM) and restart it again with no changes, the atoms related to the polymorphic type don't get initialized and I get an error when I try to .types
those, because the .types
function uses String.to_existing_atom
, which comes from the correct mindset that one shouldn't flood the BEAM with new atoms, I guess.
9:16:53 › iex -S mix
Erlang/OTP 25 [erts-13.0.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
Interactive Elixir (1.13.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> PolymorphicEmbed.types(PolymorphicEmbedBug, :channel)
** (ArgumentError) errors were found at the given arguments:
* 1st argument: not an already existing atom
:erlang.binary_to_existing_atom("sms", :utf8)
(polymorphic_embed 3.0.0) lib/polymorphic_embed.ex:370: anonymous fn/1 in PolymorphicEmbed.types/2
(elixir 1.13.2) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
I was considering giving you the hint to just use String.to_atom
for this, but actually I think it's correct not spamming the BEAM with new atoms everytime. What do you think? Is there some way one could work around this, or is a fix possible?
The function to_form
(called by polymorphic_embed_inputs_for
) is hardcoded to use __type__
as a hidden field. If the embedded schema is configured with a different type_field
, the changeset will be invalid. I'm working around this for now by manually adding another hidden field with the correct field name.
Hidden field for reference:
Hi
I have an app where people can play game like Werewolf, Pictionary. Players can spin up a room and share a 4 letter code with their friends so that they can join the same room. The schema for this is as follows:
schema "rooms" do
field :code, :string
field :game_id, :string
field :completed_at, :utc_datetime
field :data, :map
timestamps()
end
All this while, I was supporting simple games and didn't have much need for strict validation of what goes into the data
column. But now, I'm coding up a Sudoku game where I want to validate the data that goes into the data
column using embedded_schemas
. Since there are lots of existing rows without existing __type__
value in the data, is there a way to fallback to :map
type? Perhaps this can be added as an additional option for on_type_not_found
I like having the key to explicitly differentiate between the embeds. It works very nicely with TypeScript's discriminated unions on the frontend. But some other places in my system already use the :type
key without the underscores.
Is there a way to add a possibility to change the key? Thanks!
An example:
defmodule A.Parent do
# ...
schema "parents" do
has_many(:children, A.Child)
end
def changeset(%__MODULE__{} = struct, attrs) do
struct
|> cast_assoc(:children)
end
end
defmodule A.Child do
schema "children" do
# ...
polymorphic_embeds_one(:something, )
end
end
params = %{
# ...
children: [
%{
something: ... # Here contains an error
},
%{
something: ...
}
]
}
When a changeset is inserted directly at the Parent
level, but contains errors in the polymorphic embed of something
at the Child
level, PolymorphicEmbed.traverse_errors/2
when run on the resulting changeset still returns an empty map.
Not sure if Ecto.Changeset.traverse_errors/2
has the same behavior. At least the documentation of the Ecto function says
Traverses changeset errors and applies the given function to error messages.
This function is particularly useful when associations and embeds
are cast in the changeset as it will traverse all associations and
embeds and place all errors in a series of nested maps.
Thanks for the library. This is not really an issue with the library but a question that I have. Did not know where to put it - hence made it an issue.
The README provided clear details on how to define the schema. I want to know how to define the migration for the table reminders
. What does polymorphic_embded
expect? Two fields type
and channel_id
without referential integrity constraints? Or two fields sms_id
and email_id
?
Hi there, especially @mathieuprog.
First thanks for this great Library, it has helped us a lot and is a joy to use ❤️
It seems though that you might not had time or reason to work on it for some time and there are some bugs we've been running into, which even have open MRs (e.g. fixing compile time dependencies).
Therefore I wanted to ask if you're still interested in maintaining the library in the future or if we can find a way to keep it updated.
Again: thanks for all the work, and just to make sure: this is not intended to blame you for not working at the library at all, I completely understand that you've been doing this on your own time and there are good reasons to not continue to do it, I only want to try to find a way to keep the project up to date.
We are extremely excited about this library and ran into a subtle issue for our application.
While cast_polymorphic_embed
works perfectly for us, we have a situation where we need to create a record out of an already cast embed. I.e we want to move the value from one persisted record to another record.
I tried, as a work around, to use Map.from_struct
and then re-cast the data, but the issue here is that the Struct that is exposed doesn't have the __type__
field and our casting very much needs it as we can't infer the type since many sub-types share the same fields (don't ask :) )
I'm going to go down the path of using get_polymorphic_type/3
and re-casting, though this isn't working, either, yet for reasons I haven't yet determined.
That said, I believe the correct answer to this is to implement put_polymorphic_embed
so I can just drop in an existing value, somehow, instead of having to re-cast it. What do you think?
Howdy,
Thank you for the fantastic library!
I'm trying to use it with the new SQLite3 ecto adapter and I'm running into an error loading the embed after a successful save.
** (FunctionClauseError) no function clause matching in PolymorphicEmbed.do_get_polymorphic_module_from_map/3
The following arguments were given to PolymorphicEmbed.do_get_polymorphic_module_from_map/3:
# 1
"{\"last_received_at\":\"2021-05-01T21:47:26.360911Z\",\"last_venue_timestamp\":\"2021-05-01T21:47:26.360906Z\",\"venue_order_id\":\"9e7b5696-b6fe-4f89-b348-f89369456c55\",\"__type__\":\"accept_create\"}"
# 2
"__type__"
# 3
[%{identify_by_fields: [], module: Tai.NewOrders.Transitions.CreateError, type: "create_error"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.AcceptCreate, type: "accept_create"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Open, type: "open"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.PendCancel, type: "pend_cancel"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.AcceptCancel, type: "accept_cancel"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.CancelError, type: "cancel_error"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Cancel, type: "cancel"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.PendAmend, type: "pend_amend"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.AcceptAmend, type: "accept_amend"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.AmendError, type: "amend_error"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Amend, type: "amend"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Fill, type: "fill"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.PartialFill, type: "partial_fill"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Expire, type: "expire"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Reject, type: "reject"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Skip, type: "skip"}]
Attempted function clauses (showing 1 out of 1):
defp do_get_polymorphic_module_from_map(%{} = attrs, type_field, types_metadata)
stacktrace:
(polymorphic_embed 1.3.4) lib/polymorphic_embed.ex:260: PolymorphicEmbed.do_get_polymorphic_module_from_map("{\"last_received_at\":\"2021-05-01T21:47:26.360911Z\",\"last_venue_timestamp\":\"2021-05-01T21:47:26.360906Z\",\"venue_order_id\":\"9e7b5696-b6fe-4f89-b348-f89369456c55\",\"__type__\":\"accept_create\"}", "__type__", [%{identify_by_fields: [], module: Tai.NewOrders.Transitions.CreateError, type: "create_error"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.AcceptCreate, type: "accept_create"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Open, type: "open"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.PendCancel, type: "pend_cancel"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.AcceptCancel, type: "accept_cancel"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.CancelError, type: "cancel_error"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Cancel, type: "cancel"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.PendAmend, type: "pend_amend"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.AcceptAmend, type: "accept_amend"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.AmendError, type: "amend_error"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Amend, type: "amend"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Fill, type: "fill"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.PartialFill, type: "partial_fill"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Expire, type: "expire"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Reject, type: "reject"}, %{identify_by_fields: [], module: Tai.NewOrders.Transitions.Skip, type: "skip"}])
(polymorphic_embed 1.3.4) lib/polymorphic_embed.ex:222: PolymorphicEmbed.load/3
(ecto 3.6.1) lib/ecto/type.ex:894: Ecto.Type.process_loaders/3
(ecto 3.6.1) lib/ecto/repo/queryable.ex:406: Ecto.Repo.Queryable.struct_load!/6
(ecto 3.6.1) lib/ecto/repo/queryable.ex:238: anonymous fn/5 in Ecto.Repo.Queryable.preprocessor/3
(elixir 1.11.4) lib/enum.ex:1411: Enum."-map/2-lists^map/1-0-"/2
(ecto 3.6.1) lib/ecto/repo/queryable.ex:229: Ecto.Repo.Queryable.execute/4
(ecto 3.6.1) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
(tai 0.0.65) lib/tai/commander.ex:117: Tai.Commander.handle_call/3
(stdlib 3.14.2) gen_server.erl:715: :gen_server.try_handle_call/4
(stdlib 3.14.2) gen_server.erl:744: :gen_server.handle_msg/6
(stdlib 3.14.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
stacktrace:
(elixir 1.11.4) lib/gen_server.ex:1027: GenServer.call/3
(tai 0.0.65) lib/tai/iex/commands/order_transitions.ex:18: Tai.IEx.Commands.OrderTransitions.order_transitions/1
(ex_unit 1.11.4) lib/ex_unit/capture_io.ex:191: ExUnit.CaptureIO.do_capture_gl/2
(ex_unit 1.11.4) lib/ex_unit/capture_io.ex:149: ExUnit.CaptureIO.do_capture_io/3
test/tai/iex/commands/order_transitions_test.exs:52: (test)
I'm not super familiar with the requirements of ecto adapters but the crux of the issue is that the SQLite adapter is returning a string for the map
field and polymorphic_embed
expects that to already be turned into an Elixir map. I'm not sure if this is something wrong in ecto_sqlite3
or does polymorphic_embed
need to call the currently ignored loader?
def load(data, _loader, %{types_metadata: types_metadata, type_field: type_field}) do
case do_get_polymorphic_module_from_map(data, type_field, types_metadata) do
nil -> raise_cannot_infer_type_from_data(data)
module when is_atom(module) -> {:ok, Ecto.embedded_load(module, data, :json)}
end
end
This is the migration
defmodule Tai.NewOrders.OrderRepo.Migrations.CreateOrderTransitions do
use Ecto.Migration
def change do
create table(:order_transitions, primary_key: false) do
add(:id, :uuid, null: false, primary_key: true)
add(:order_client_id, references(:orders, column: :client_id, type: :uuid))
add(:transition, :map, null: false)
timestamps()
end
end
end
And schema definition
defmodule Tai.NewOrders.OrderTransition do
use Ecto.Schema
import Ecto.Changeset
import PolymorphicEmbed, only: [cast_polymorphic_embed: 3]
alias Tai.NewOrders.{Order, Transitions}
@type client_id :: Ecto.UUID.t()
@type t :: %__MODULE__{}
@primary_key {:id, Ecto.UUID, autogenerate: true}
@timestamps_opts [autogenerate: {Tai.DateTime, :timestamp, []}, type: :utc_datetime_usec]
schema "order_transitions" do
belongs_to(:order, Order, source: :order_client_id, references: :client_id, foreign_key: :order_client_id, type: Ecto.UUID)
field(:transition, PolymorphicEmbed,
types: [
create_error: Transitions.CreateError,
accept_create: Transitions.AcceptCreate,
open: Transitions.Open,
pend_cancel: Transitions.PendCancel,
accept_cancel: Transitions.AcceptCancel,
cancel_error: Transitions.CancelError,
cancel: Transitions.Cancel,
pend_amend: Transitions.PendAmend,
accept_amend: Transitions.AcceptAmend,
amend_error: Transitions.AmendError,
amend: Transitions.Amend,
fill: Transitions.Fill,
partial_fill: Transitions.PartialFill,
expire: Transitions.Expire,
reject: Transitions.Reject,
skip: Transitions.Skip
],
on_type_not_found: :raise,
on_replace: :update
)
timestamps()
end
@doc false
def changeset(order_transition, attrs) do
order_transition
|> cast(attrs, [:order_client_id])
|> cast_polymorphic_embed(:transition, required: true)
|> validate_required([:order_client_id])
end
end
It seems that I'm unable to use it with ex_machina. Not sure if I'm doing something wrong here. I get the error
** (RuntimeError) Elixir.PolymorphicEmbed must not be casted using Ecto.Changeset.cast/4, use Elixir.PolymorphicEmbed.cast_polymorphic_embed/2 instead.
so it seems that ex_machina
by default tries to run cast
?
Example:
def entry_factory do
%Entry{
# ...
data: %{
field_1: 123,
field_2: 123,
field_3: 123
}
}
end
Schema:
schema "entries" do
# ...
field :data, PolymorphicEmbed,
types: @types_map,
type_field: :type,
on_type_not_found: :raise,
on_replace: :update
end
Trying to use a call to build/1
at the :data
key seems to yield the same result.
Stacktrace of the error:
code: entry = insert(:entry)
stacktrace:
(polymorphic_embed 2.0.0) lib/polymorphic_embed.ex:247: PolymorphicEmbed.cast/2
(ex_machina 2.7.0) lib/ex_machina/ecto_strategy.ex:72: ExMachina.EctoStrategy.cast_value/3
(ex_machina 2.7.0) lib/ex_machina/ecto_strategy.ex:58: anonymous fn/2 in ExMachina.EctoStrategy.cast_all_fields/1
(elixir 1.13.4) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
(ex_machina 2.7.0) lib/ex_machina/ecto_strategy.ex:49: ExMachina.EctoStrategy.cast/1
(ex_machina 2.7.0) lib/ex_machina/ecto_strategy.ex:25: ExMachina.EctoStrategy.handle_insert/2
(BTW, I don't like ex_machina
very much, though the codebase that I'm working with already uses it.)
Hello. I'm trying to tidy up some code for a SQL query builder. Presently, I manually assign the inputs' name
and id
fields and save it all as JSONB.
How are you distinguishing between types in the frontend? The example in the docs only shows the sms
part, when the channel can be either sms
or email
.
And have you had any luck using polymorphic_embed_inputs_for
for an {:array, PolymorphicEmbed}
?
Given these schemas:
defmodule Polypipe.EmbedQuery do
defmodule EmbedRule do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field :field, :string
field :operator, Ecto.Enum, values: ~w(lt le eq neq ge gt like ilike notlike notilike in notin null notnull)a
field :value, :string
end
def changeset(rule, params) do
rule
|> cast(params, [:field, :value, :operator])
end
end
defmodule EmbedRuleGroup do
use Ecto.Schema
import Ecto.Changeset
import PolymorphicEmbed, only: [cast_polymorphic_embed: 3]
alias Polypipe.EmbedQuery.{EmbedRule, EmbedRuleGroup}
embedded_schema do
field :condition, :string
field :rules, {:array, PolymorphicEmbed},
types: [
rule_group: [module: EmbedRuleGroup, identify_by_fields: [:condition, :rules]],
rule: [module: EmbedRule, identify_by_fields: [:field, :operator]]
],
on_type_not_found: :raise,
on_replace: :delete
end
def changeset(group, params) do
group
|> cast(params, [:condition])
|> cast_polymorphic_embed(:rules, required: true)
end
end
use Ecto.Schema
import Ecto.Changeset
schema "embed_queries" do
field :name, :string
embeds_one :rule_group, __MODULE__.EmbedRuleGroup, on_replace: :delete
end
def changeset(data \\ %__MODULE__{}, params) do
data
|> cast(params, [:name])
|> cast_embed(:rule_group)
end
end
And this basic template (haven't use components yet as I just wanted to see if it works).
<section class="row">
<article class="column">
<h2>Form</h2>
<%= form_for @changeset, Routes.page_path(@conn, :create), fn f -> %>
<%= label f, :name %>
<%= text_input f, :name %>
<%= inputs_for f, :rule_group, fn f_rule_group -> %>
<h2>Rule Group Form</h2>
<%= label f_rule_group, :condition %>
<%= text_input f_rule_group, :condition %>
<%= polymorphic_embed_inputs_for f_rule_group, :rules, :rule, fn f_rule -> %>
<h2>Poly Form</h2>
<%= case f_rule.data do %>
<% %Polypipe.EmbedQuery.EmbedRule{} -> %>
<h3>Rule <%= f_rule.index %></h3>
<%= text_input f_rule, :field %>
<%= text_input f_rule, :operator %>
<%= text_input f_rule, :value %>
<% %Polypipe.EmbedQuery.EmbedRuleGroup{} -> %>
<h3>Rule -> Rule Group <%= f_rule.index %></h3>
<%= label f_rule, :condition %>
<%= text_input f_rule, :condition %>
<%= polymorphic_embed_inputs_for f_rule, :rules, :rule, fn f_rule_rule_group_rule -> %>
<h2>Deep Form</h2>
<h3>Rule <%= f_rule_rule_group_rule.index %></h3>
<%= text_input f_rule_rule_group_rule, :field %>
<%= text_input f_rule_rule_group_rule, :operator %>
<%= text_input f_rule_rule_group_rule, :value %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
</article>
</section>
and this data
%Polypipe.EmbedQuery{
name: "nombre",
rule_group: %Polypipe.EmbedQuery.EmbedRuleGroup{
condition: "and",
rules: [
%Polypipe.EmbedQuery.EmbedRule{
field: "field_a",
operator: :eq,
value: "a"
},
%Polypipe.EmbedQuery.EmbedRuleGroup{
condition: "or",
rules: [
%Polypipe.EmbedQuery.EmbedRule{
field: "field_b",
operator: :eq,
value: "b"
},
%Polypipe.EmbedQuery.EmbedRule{
field: "field_c",
operator: :eq,
value: "c"
}
]
}
]
}
}
This kind of works, but it clearly isn't right because I'm telling it these polymorphic embeds are rules
, when really they're a list of rules
and rule_groups
.
Hey, thx for the so nice library.
Now a standard way to handling errors in ecto changeset Ecto.changeset.traverse_errors/2
doesn't work as expected with PolymorphicEmbed .
In ecto it works only for maps with keys %{cardinality: :one}
or %{cardinality: :many}
and tag in @relations [:embed, :assoc]
Is there any way to include {:parameterized, PolymorphicEmbed, %{}}
to handling by traverse_erorrs, or create a new function that will be doing the same for PolymorphicEmbed
?
Thx
This commit introduces a regression for me: 0081ac2
Installed Version: 1.3.3
The to_form
function only works for me if the changeset is invalid, as soon as it is valid, I get an ArgumentError
:
[error] GenServer #PID<0.1829.0> terminating
** (FunctionClauseError) no function clause matching in PolymorphicEmbed.HTML.Form.do_get_errors/1
(polymorphic_embed 1.3.3) lib/html/form.ex:70: PolymorphicEmbed.HTML.Form.do_get_errors(%Acme.CaseContext.ProtocolEntry.Sms{delivery_receipt_id: nil, text: "f", uuid: nil})
(polymorphic_embed 1.3.3) lib/html/form.ex:26: PolymorphicEmbed.HTML.Form.to_form/5
(acme_web 0.0.0-noversion) lib/acme_web/live/polymorphic_inputs.ex:20: anonymous fn/4 in AcmeWeb.PolimorphicInputs.render/1
(phoenix_live_view 0.15.3) lib/phoenix_live_view/diff.ex:342: Phoenix.LiveView.Diff.traverse/6
(phoenix_live_view 0.15.3) lib/phoenix_live_view/diff.ex:430: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/6
(elixir 1.11.3) lib/enum.ex:2193: Enum."-reduce/3-lists^foldl/2-0-"/3
(phoenix_live_view 0.15.3) lib/phoenix_live_view/diff.ex:342: Phoenix.LiveView.Diff.traverse/6
(phoenix_live_view 0.15.3) lib/phoenix_live_view/diff.ex:430: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/6
(elixir 1.11.3) lib/enum.ex:2193: Enum."-reduce/3-lists^foldl/2-0-"/3
(phoenix_live_view 0.15.3) lib/phoenix_live_view/diff.ex:342: Phoenix.LiveView.Diff.traverse/6
(phoenix_live_view 0.15.3) lib/phoenix_live_view/diff.ex:430: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/6
(elixir 1.11.3) lib/enum.ex:2193: Enum."-reduce/3-lists^foldl/2-0-"/3
(phoenix_live_view 0.15.3) lib/phoenix_live_view/diff.ex:342: Phoenix.LiveView.Diff.traverse/6
(phoenix_live_view 0.15.3) lib/phoenix_live_view/diff.ex:430: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/6
(elixir 1.11.3) lib/enum.ex:2193: Enum."-reduce/3-lists^foldl/2-0-"/3
(phoenix_live_view 0.15.3) lib/phoenix_live_view/diff.ex:342: Phoenix.LiveView.Diff.traverse/6
(phoenix_live_view 0.15.3) lib/phoenix_live_view/diff.ex:584: Phoenix.LiveView.Diff.render_component/9
(phoenix_live_view 0.15.3) lib/phoenix_live_view/diff.ex:529: anonymous fn/5 in Phoenix.LiveView.Diff.render_pending_components/6
(elixir 1.11.3) lib/enum.ex:2193: Enum."-reduce/3-lists^foldl/2-0-"/3
(stdlib 3.13.2) maps.erl:233: :maps.fold_1/3
Changeset at time of error:
#Ecto.Changeset<
action: :validate,
changes: %{
entry: %Acme.CaseContext.ProtocolEntry.Sms{
delivery_receipt_id: nil,
text: "f",
uuid: nil
},
type: "sms"
},
errors: [],
data: #Acme.CaseContext.ProtocolEntry<>,
valid?: true
>
Changeset when working:
#Ecto.Changeset<
action: :validate,
changes: %{
entry: #Ecto.Changeset<
action: :insert,
changes: %{text: "fffffffffffffffffffffffff"},
errors: [delivery_receipt_id: {"can't be blank", [validation: :required]}],
data: #Hygeia.CaseContext.ProtocolEntry.Sms<>,
valid?: false
>,
type: "sms"
},
errors: [],
data: #Hygeia.CaseContext.ProtocolEntry<>,
valid?: false
>
Currently, from what I can tell, this library requires a changeset for all inserts.
We have a use case where we need to create 10s of thousands of records using insert_all that makes use of a polymorphic_embeds field.
We are using insert_all
for performance reasons as looping through and making 10k inserts is an issue.
This results in the following error
| ** (RuntimeError) polymorphic_embed is not able to add an autogenerated key without casting through cast_polymorphic_embed/3
| (polymorphic_embed 2.0.1) lib/polymorphic_embed.ex:282: PolymorphicEmbed.dump/3
| (ecto 3.9.2) lib/ecto/type.ex:941: Ecto.Type.process_dumpers/3
| (ecto 3.9.2) lib/ecto/repo/schema.ex:1006: Ecto.Repo.Schema.dump_field!/6
When using embeds_many
, the same problem can exist BUT if you pass in a struct with all the necessary fields, the insert succeeds - this is discussed here: https://elixirforum.com/t/using-repo-insert-all-when-your-schema-has-embeds-many/14219
Is there a way to force a known json blob into the field when using insert_all?
Many thanks!
This package just went from 1.3.x to 1.6.x within 3 days and there's no changelog or change notes in the releases. Can you please describe what changed somewhere?
When using polymorphic_embeds_one
and polymorphic_embeds_many
this library creates compile time dependencies between the parent module and the polymorphic embed modules. This causes huge dependency graphs in which changing a file requires a recompilation of many others.
This same problem existed in Ecto some time ago as reported in elixir-ecto/ecto#1610. The Ecto team fixed this by disabling the lexical tracker for the association modules as shown in elixir-ecto/ecto#1670.
Given that Polymorphic Embed aims to mimic Ecto's embeds_one
and embeds_many
. Would it make sense to use the same approach as Ecto did to fix this issue?
I try get id in polymorphic object, but it always nil.
schema "presets" do
field :name, :string
field :show, :boolean, default: false
belongs_to :user, User
field :fields, {:array, PolymorphicEmbed},
types: [
string: StringField,
text: TextField
],
type_field: :type,
on_type_not_found: :raise,
on_replace: :delete
timestamps()
end
StringField
@primary_key {:id, :binary_id, autogenerate: true}
embedded_schema do
field :title, :string
field :type, :string
end
But no id when insert in DB
{:ok,
%Preset{
__meta__: #Ecto.Schema.Metadata<:loaded, "presets">,
fields: [
%TextField{
id: nil,
title: "Sec text",
type: nil
},
%StringField{
id: nil,
title: "Sec string",
type: nil
}
],
id: 1,
inserted_at: ~N[2021-01-14 21:47:41],
name: "Second preset",
show: false,
updated_at: ~N[2021-01-14 21:47:41],
user: #Ecto.Association.NotLoaded<association :user is not loaded>,
user_id: 1
}}
When i use embeds_many all ok.
Can you help me?
Hi, I was trying to figure out why the type field is nil
after using the cast_polymorphic_embed
function. After reading the code I noticed the type field is only populated inside the dump
ecto type implementation. Is there any reason why this is so?
This is the struct after an insert:
%PolymorphicEmbed.Reminder{
__meta__: #Ecto.Schema.Metadata<:loaded, "reminders">,
channel: %PolymorphicEmbed.Channel.SMS{
country_code: 1,
my_type_field: nil,
number: "02/807.05.53"
},
date: ~U[2020-05-28 02:57:19Z],
id: 1,
inserted_at: ~N[2021-03-18 20:35:34],
text: "This is an SMS reminder true",
updated_at: ~N[2021-03-18 20:35:34]
}
and this is the struct after "reloading" the data:
%PolymorphicEmbed.Reminder{
__meta__: #Ecto.Schema.Metadata<:loaded, "reminders">,
channel: %PolymorphicEmbed.Channel.SMS{
country_code: 1,
my_type_field: "sms",
number: "02/807.05.53"
},
date: ~U[2020-05-28 02:57:19Z],
id: 1,
inserted_at: ~N[2021-03-18 20:35:34],
text: "This is an SMS reminder true",
updated_at: ~N[2021-03-18 20:35:34]
}
Notice how the my_type_field
value only set when I load the data through Repo.one
here.
Would you mind if I opened a PR to add a workflow config for Github actions?
Is it possible to embed a form with polymorphic_embed_inputs_for and pass in a phx-action to trigger a validation? Can not get that working.
Ecto's Ecto.Changeset.cast_embed/3
allows specifying the :with
option, where you can not only specify which changeset to use, but also whether to pass any additional arguments. I find myself missing this function when working with nested validations dependent on some parent's data.
When I use Phoenix.HTML.Form.inputs_for/2
on a list of embeds (i.e. defined using embeds_many), the name of the fields for the embeds generated with Phoenix.HTML.Form.input_name/2
includes an index ([0], [1] etc.), which is necessary to submit the list of the embeds in the form as a list and not as a single embed.
When I use PolymorphicEmbed.HTML.Form.polymorphic_embed_inputs_for/2
however, no index is generated. See example below:
Mix.install([{:ecto, "~> 3.9.4"}, {:polymorphic_embed, "~> 3.0.5"}, {:phoenix_html, " ~> 3.2"}, {:phoenix_ecto, "~> 4.4"}])
defmodule ChildSchema do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field(:name, :string)
field(:age, :integer)
end
def changeset(source \\ %__MODULE__{}, changes) do
cast(source, changes, [:name, :age])
|> validate_required([:name])
end
end
defmodule ParentSchema do
use Ecto.Schema
import Ecto.Changeset
import PolymorphicEmbed
embedded_schema do
embeds_many(:children, ChildSchema)
polymorphic_embeds_many(:polymorphic_children,
types: [
child_schema: ChildSchema
],
on_replace: :delete,
on_type_not_found: :raise
)
end
def changeset(source \\ %__MODULE__{}, changes) do
cast(source, changes, [])
|> cast_embed(:children)
|> cast_polymorphic_embed(:polymorphic_children)
end
end
children_params = [%{name: "Tic", age: 10}, %{name: "Tac", age: 12}]
form = ParentSchema.changeset(%{
children: children_params,
polymorphic_children: children_params |> Enum.map(&Map.put(&1, :__type__, :child_schema)),
})
|> Phoenix.HTML.FormData.to_form([])
for f <- Phoenix.HTML.Form.inputs_for(form, :children) do
Phoenix.HTML.Form.input_name(f, :name) |> IO.inspect(label: "input_name(f, :name), ordinary embed")
Phoenix.HTML.Form.input_name(f, :age)|> IO.inspect(label: "input_name(f, :age), ordinary embed")
end
for f <- PolymorphicEmbed.HTML.Form.polymorphic_embed_inputs_for(form, :polymorphic_children) do
Phoenix.HTML.Form.input_name(f, :name) |> IO.inspect(label: "input_name(f, :name), polymorphic embed")
Phoenix.HTML.Form.input_name(f, :age)|> IO.inspect(label: "input_name(f, :age), polymorphic embed")
end
Output:
input_name(f, :name), ordinary embed: "parent_schema[children][0][name]"
input_name(f, :age), ordinary embed: "parent_schema[children][0][age]"
input_name(f, :name), ordinary embed: "parent_schema[children][1][name]"
input_name(f, :age), ordinary embed: "parent_schema[children][1][age]"
input_name(f, :name), polymorphic embed: "parent_schema[polymorphic_children][name]"
input_name(f, :age), polymorphic embed: "parent_schema[polymorphic_children][age]"
input_name(f, :name), polymorphic embed: "parent_schema[polymorphic_children][name]"
input_name(f, :age), polymorphic embed: "parent_schema[polymorphic_children][age]"
This prevents forms with lists of polymorphic embeds from being correctly rendered/submitted.
AFAICS, this is a bug. If this is not the case, I would like to know what I'm missing and what the suggested workaround might be.
Thanks!
👋 I'm currently updating to 0.13.0 and I ran into an issue with cast_polymorphic_embed mixing both atom and string keys. I guess this can be fixed by converting both data_for_field and params_for_field into string keys (as atoms have a limit)?
$ mix compile
==> polymorphic_embed
Compiling 2 files (.ex)
== Compilation error in file lib/html/form.ex ==
** (CompileError) lib/html/form.ex:4: cannot import Phoenix.HTML.Form.hidden_inputs_for/1 because it is undefined or private
(elixir 1.10.2) src/elixir_import.erl:64: :elixir_import.calculate/6
(elixir 1.10.2) src/elixir_import.erl:18: :elixir_import.import/4
could not compile dependency :polymorphic_embed, "mix compile" failed. You can recompile this dependency with "mix deps.compile polymorphic_embed", update it with "mix deps.update polymorphic_embed" or clean it with "mix deps.clean polymorphic_embed"
I've an umbrella app with all my schemas, which understandably doesn't depend on phoenix_html
. The above compilation error shows up when I try to compile the project. I'm using phoenix_html
on another app in the same project so I've an easy workaround by depending on it from this app too, but it'd be nice to be able to use this package without the Phoenix dependency :)
Edit: I'm on version 0.4.0 of polymorphic_embed
.
When casting a data structure in Changesets that the library does not expect, it will trigger a runtime error such as
** (RuntimeError) could not infer polymorphic embed from data %{}
Ideally, I'd rather have this as an :error
as would be produced by Ecto.Type.cast/1
. This way, I could get the error back to the Changeset instead of having to defensively try to rescue
errors with Changeset casting.
Could this be improved at library level, or am I somehow using this wrong currently?
Using https://github.com/elixir-sqlite/ecto_sqlite3 , and it dumps to string that can't be later read properly with SQLite, it stores it like this in a JSON field in SQLite:
"{\"property_type\":\"foo\",\"__type__\":\"something\"}"
instead of like this:
{"property_type": "foo", "__type__": "something"}
Means functions like json_extract etc won't work natively in SQLite.
I believe it's because when dumping it's done this way:
dumper.(:map, map)
So with SQLite, this dumps as a string:
{:ok,
"{\"foo\":\"x\",\"bar\":\"2021-12-03T07:22:16.086910Z\",\"baz\":\"xxx\",\"__type__\":\"something\"}"}
But with Postgres it dumps as a map:
{:ok,
%{
:something => "hello",
"__type__" => :"Whatever"
}}
Not sure if this is an ecto_sqlite3 problem or a polymorphic_embed issue, but the fix will probably end up changing the loader as well.
We are using ex_machina
to seed schema and data in our tests. However, when trying to seed a schema with a nil
polymorphic field, we face the following error:
(UndefinedFunctionError) function nil.__struct__/0 is undefined. If you are using the dot syntax, such as map.field or module.function, make sure the left side of the dot is an atom or a map
ExMachina uses cast/2
, which never pattern match on a nil
value. I forked the library to provide reproduction steps in a separate codebase not to include an additional dependency here!
> mix test test/polymorphic_embed_test.exs:114
…
1) test inserting a null embed in ExMachina factory (PolymorphicEmbedTest)
test/polymorphic_embed_test.exs:114
** (UndefinedFunctionError) function nil.__struct__/0 is undefined. If you are using the dot syntax, such as map.field or module.function, make sure the left side of the dot is an atom or a map
code: Factory.insert(
stacktrace:
nil.__struct__()
(elixir 1.10.4) lib/kernel.ex:2182: Kernel.struct/3
(polymorphic_embed 0.12.0) lib/polymorphic_embed.ex:90: PolymorphicEmbed.cast_to_changeset/2
(polymorphic_embed 0.12.0) lib/polymorphic_embed.ex:68: PolymorphicEmbed.cast/2
(ex_machina 2.4.0) lib/ex_machina/ecto_strategy.ex:62: ExMachina.EctoStrategy.cast_value/3
(ex_machina 2.4.0) lib/ex_machina/ecto_strategy.ex:48: anonymous fn/2 in ExMachina.EctoStrategy.cast_all_fields/1
(elixir 1.10.4) lib/enum.ex:2111: Enum."-reduce/3-lists^foldl/2-0-"/3
(ex_machina 2.4.0) lib/ex_machina/ecto_strategy.ex:39: ExMachina.EctoStrategy.cast/1
(ex_machina 2.4.0) lib/ex_machina/ecto_strategy.ex:25: ExMachina.EctoStrategy.handle_insert/2
test/polymorphic_embed_test.exs:116: (test)
Finished in 0.4 seconds
14 tests, 1 failure, 13 excluded
👋 First of all cheers for making this library!
I'm currently experiencing some issues with validating nested embeds and hope ya can provide some insights. I have the following data-structure:
%{
foo: %{
bar: %{ <= Polymorphic embed
__type__: "x",
title: "test",
images: %{ <= embeds_one
large: "https://foo.bar/x.png",
small: "https://foo.bar/x.png",
}
}
}
}
Where the field title is required and so are the fields large and small under images.
With the current implementation it will return the correct validation error for the title field (e.g. required field) but will not show any validation errors for fields under images. Any ideas on how to resolve this?
When i using polymorphic_embed with mongodb_ecto on MongoDB, below error occured:
iex(19)> PosServiceAdmin.Stores.create_store(%{ status: "approved", store: %{ __type__: :local, name: "Candy store" } })
[debug] QUERY ERROR
COMMAND [insert: "stores", documents: [[_id: #BSON.ObjectId<645bc685da6776dc01f6bd85>, status: "approved", store: [{:name, "Candy store"}, {"__type__", :local}], inserted_at: ~U[2023-05-10 16:29:57Z], updated_at: ~U[2023-05-10 16:29:57Z]]], writeConcern: %{}] [[insert: "stores", documents: [[_id: #BSON.ObjectId<645bc685da6776dc01f6bd85>, status: "approved", store: [{:name, "Candy store"}, {"__type__", :local}], inserted_at: ~U[2023-05-10 16:29:57Z], updated_at: ~U[2023-05-10 16:29:57Z]]], writeConcern: %{}]]
** (ArgumentError) invalid document containing atom and string keys: [{:name, "Candy store"}, {"__type__", :local}]
(mongodb 1.0.0) lib/bson/encoder.ex:171: BSON.Encoder.invalid_doc/1
(elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(mongodb 1.0.0) lib/bson/encoder.ex:127: BSON.Encoder.document/1
(mongodb 1.0.0) lib/bson/encoder.ex:146: anonymous fn/3 in BSON.Encoder.document/1
(elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(mongodb 1.0.0) lib/bson/encoder.ex:127: BSON.Encoder.document/1
(mongodb 1.0.0) lib/bson/encoder.ex:146: anonymous fn/3 in BSON.Encoder.document/1
(elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(mongodb 1.0.0) lib/bson/encoder.ex:127: BSON.Encoder.document/1
(mongodb 1.0.0) lib/bson/encoder.ex:146: anonymous fn/3 in BSON.Encoder.document/1
(elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(mongodb 1.0.0) lib/bson/encoder.ex:127: BSON.Encoder.document/1
(elixir 1.14.0) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(db_connection 2.4.3) lib/db_connection.ex:1317: DBConnection.maybe_encode/4
(db_connection 2.4.3) lib/db_connection.ex:695: DBConnection.execute/4
(mongodb 1.0.0) lib/mongo.ex:633: Mongo.direct_command/3
(mongodb 1.0.0) lib/mongo.ex:700: Mongo.insert_one/4
(mongodb_ecto 1.0.0) lib/mongo_ecto/connection.ex:180: Mongo.Ecto.Connection.insert/3
(ecto 3.9.5) lib/ecto/repo/schema.ex:756: Ecto.Repo.Schema.apply/4
(ecto 3.9.5) lib/ecto/repo/schema.ex:369: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
Hi, trying to see if this library is for me, but just couldnt figure out how to actually make it work.
Is there a phoenix example using this?
The current example is tied to a form, but the whole Context part is missing.
Hey all, thank you for the work on this library!
I've been trying to implement it using the README example, but without any success so far, always resulting in the following error:
<embedded-data>
}I have created a minimum reproduction repository, explaining the setup and individual reproduction steps, available here:
Note: I am fairly new to Elixir/Phoenix, so there might be something very obvious that I am missing...
Thank you for your time in advance!
First of all, thanks a ton for this great library 👍
I have the following schema (abridged):
defmodule ClientApiConfig do
schema "client_api_config" do
polymorphic_embeds_one(:args,
types: [
legal_tracker: DataIngestion.LegalTracker.ApiArgs,
counsellink: DataIngestion.Counsellink.ApiArgs,
ebiller_mock: DataIngestion.Api.DummyArgs
],
on_replace: :update,
on_type_not_found: :changeset_error
)
end
end
Then in my HTML form, following the LiveView example I have:
<%= for args_form <- polymorphic_embed_inputs_for(f, :args) do %>
<%= hidden_inputs_for(args_form) %>
<%= case get_polymorphic_type(f, ClientApiConfig, :args) do %>
<% :counsellink -> %>
... inputs for counsellink
<% :legal_tracker -> %>
...inputs for legal_tracker
<% end %>
<% end %>
(sidenote, I think the LiveView example is wrong, it should be get_polymorphic_type(f, Reminder, :channel)
and not get_polymorphic_type(channel_form, Reminder, :channel)
)
The above form works if I'm editing new data which already contains an :args
embed. If however I want to edit a new record and I start from an empty %ClientApiConfig{}
, no fields will be rendered because polymorphic_embed_inputs_for(f, :args)
returns []
(there's no :args
embed and therefore no :__type__
field in it, I guess).
How can I enforce a default type? I can do that with polymorphic_embed_inputs_for/4, but since I'm using LV, I thought I'm supposed to use polymorphic_embed_inputs_for/2. Or perhaps I should use the undocumented to_form/5?
Seems to me that polymorphic_embed_inputs_for/2
should be extended to accept a type
parameter.
Hi, great lib, it really helps on the project I'm working on.
I'm having a problem with changing the embedded type on a struct: when I create a struct with one polymorphic type and then when I try to change to other type it fails, or more specific for my situation - I create an entry with an email reminder and then I want to change it to an SMS reminder, but when I save it it remains as Email. Could you help me out please?
I wrote a failing test based on your existing tests to help you figure out what I'm trying to achieve. The changeset doesn't register the change channel change, only date and text. Is this a bug or am I just doing something wrong?
test "changing type" do
reminder_module = get_module(Reminder, true)
email_module = get_module(Channel.Email, true)
sms_module = get_module(Channel.SMS, true)
attrs = %{
date: ~U[2020-05-28 02:57:19Z],
text: "This is an Email reminder",
channel: %{
address: "[email protected]",
valid: true,
confirmed: false
}
}
insert_result =
struct(reminder_module)
|> reminder_module.changeset(attrs)
|> Repo.insert()
assert {:ok, %reminder_module{} = reminder} = insert_result
update_attrs = %{
date: ~U[2020-05-29 02:57:19Z],
text: "This is an SMS reminder",
channel: %{
my_type_field: "sms",
number: "02/807.05.53",
country_code: 1,
attempts: [],
provider: nil
}
}
update_result = reminder
|> reminder_module.changeset(update_attrs)
|> Repo.update()
assert {:ok, %reminder_module{} = updated_reminder} = update_result
reminder =
reminder_module
|> QueryBuilder.where(text: "This is an SMS reminder")
|> Repo.one()
# I expected this to be true
assert sms_module == reminder.channel.__struct__
end
Hey there! Thanks so much for this library! I've found great value in it!
I am using PolymorphicEmbed.traverse_errors/2
as a replacement for Ecto.Changeset.traverse_errors/2
and noticed that the function blows on a FunctionClauseError
if the polymorphic embed is valid. I see that if the embedded changeset is valid, it applies the changes, which results in that field converted from a changeset to a struct. When traversing errors, it appears that when merging the polymorphic keys, it attempts to call traverse_errors/2
again, but since the change is now a struct instead of a changeset, traverse_errors/2
can't match, resulting in a FunctionClauseError
.
I think we could use some better tests and examples for building forms with array fields (polymorphic_embeds_many
).
Hi,
I have an issue casting nil when the embed is not empty, If i pass in a new map, it works as expected; However when the map contains values, it fails.
I am trying to parse Telegram Bot API using Ecto embedded_schemas and polymorphic_embed
. The issue with the API is that it's not possible to match embedded type based on a single strategy:
I'm thinking of patching this library to allow "hybrid" matching mode: use 1 strategy, and if it yields more than 1 result, use second strategy on top of matched subtypes to get the final match. I'm willing to submit it as a PR to this project, but first I would need to know if you will accept it, and your feedback on the general direction.
PolymorphicEmbed.HTML.Form.to_form/5
always sets the name of the hidden input field for the type to __type__
, even if a custom type field is defined in the schema.
https://github.com/mathieuprog/polymorphic_embed/blob/master/lib/polymorphic_embed/html/form.ex#L153
See #68 for tests.
First time using this library, I really like it!
I have a question about type_field and the embed map. Is it possible to base the type field on another field on the record? In this way, the type would not be stored in the map at all?
Here's an example record to show what I mean:
id | 8f161964-55ee-48f4-8ea5-1a7c22401f28
type | 2000
metadata | {"__type__": "session_page_view", "hub_page_id": 108}
In this case, the type
is an Ecto.Enum that maps to atoms. The __type__
key ends up duplicating this value onto the record. I tried type_field
, but it requires the value in the embed.
given the following embed:
defmodule Embedded do
@moduledoc false
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field :x, :string
end
def changeset(embedded, params) do
embedded
|> cast(params, [:x])
|> validate_required([:x])
end
end
And the following usage:
defmodule Parent do
@moduledoc false
use Ecto.Schema
import PolymorphicEmbed, only: [cast_polymorphic_embed: 2]
schema "patches" do
field :data, PolymorphicEmbed, types: [
embedded: Embedded
]
timestamps()
end
@doc false
def create_changeset(%Patch{} = patch) do
patch
|> cast(%{}, [])
|> cast_polymorphic_embed(:data)
|> validate_required([:data])
end
end
I was expecting the changeset to run, but they're not. (checked via tests, by removing x attribute expecting things to fail, also the coveralls says it's never run)
Hello!
In your documentation, one of the feature is the ability to add a changeset
Run changeset validations when a changeset/2 function is present (when absent, the library will introspect the fields to cast)
What I found is that the first parameter is always an empty struct and not the actual existing data. Meaning that even if I do not cast some field, it will be reset to nil
because of the data.
Is there something I do wrong or a known workaround? Is it known and as designed?
Thank you!
Here's a simple example,
schema "parent" do
field(:child, Child)
end
def changeset(parent, attrs) do
cast(parent, attrs, ~w(child)a)
end
embedded_schema "custom_child" do
field(:first, :string)
field(:second, :string)
end
def changeset(child, attrs) do
cast(child, attrs, ~w(first)a)
end
In this case, even if my existing child has a value for second
, it won't be kept, unless I pass as attrs
and cast it
Hello!
As of 3.0.3
, if I pass in
%{ "channel": "an_unknown_channel", ...}
to a schema with:
polymorphic_embeds_one(:channel,
types: [
known_channel: ... # some atom
]
)
You will receive this error:
** (ArgumentError) errors were found at the given arguments:
* 1st argument: not an already existing atom
code: SomeSchema.load(invalid_payload)
stacktrace:
:erlang.binary_to_existing_atom("an_unknown_channel", :utf8)
(polymorphic_embed 3.0.3) lib/polymorphic_embed.ex:389: PolymorphicEmbed.maybe_to_existing_atom/1
(polymorphic_embed 3.0.3) lib/polymorphic_embed.ex:383: PolymorphicEmbed.get_metadata_for_type/2
(polymorphic_embed 3.0.3) lib/polymorphic_embed.ex:349: PolymorphicEmbed.do_get_polymorphic_module_for_type/2
(polymorphic_embed 3.0.3) lib/polymorphic_embed.ex:156: PolymorphicEmbed.cast_polymorphic_embeds_one/5
I believe this is due to the changes in 3.0.3
that introduce String.to_existing_atom(...)
. This raises on no found atom, but this library shouldn't raise an error unless you have on_not_found: :raise
set.
Observed behavior: When I build a changeset with ordinary embeds, these embeds stay changesets. When I build the changesets with polymorphic embeds, the embeds are turned into structs if they're valid.
Expected behavior: I would expect polymorphic embeds to be kept as changeset until I call apply_changes
or apply_action
on the parent changeset, just as normal embeds do.
Ecto version: 3.9.4
polymorphic_embed version: 3.0.5
Here's a working example:
Mix.install([{:ecto, "~> 3.9.4"}, {:polymorphic_embed, "~> 3.0.5"}])
defmodule ChildSchema do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field(:name, :string)
field(:age, :integer)
end
def changeset(source \\ %__MODULE__{}, changes) do
cast(source, changes, [:name, :age])
end
end
defmodule ParentSchema do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
embeds_many(:children, ChildSchema)
end
def changeset(source \\ %__MODULE__{}, changes) do
cast(source, changes, [])
|> cast_embed(:children)
end
end
defmodule ParentSchemaWithPolymorphicChildren do
use Ecto.Schema
import PolymorphicEmbed
import Ecto.Changeset
embedded_schema do
polymorphic_embeds_many(:children,
types: [
child_schema: ChildSchema
],
on_replace: :delete,
on_type_not_found: :raise
)
end
def changeset(source \\ %__MODULE__{}, changes) do
cast(source, changes, [])
|> cast_polymorphic_embed(:children)
end
end
ParentSchema.changeset(%{children: [%{name: "Tic", age: 10}, %{name: "Tac", age: 12}]})
|> IO.inspect(label: "changeset for schema with traditional embeds: embeds are still changesets")
ParentSchemaWithPolymorphicChildren.changeset(%{
children: [
%{name: "Tic", age: 10, __type__: "child_schema"},
%{name: "Tac", age: 12, __type__: "child_schema"}
]
})
|> IO.inspect(
label: "changeset for schema with polymorphic embeds: embeds are have already been applied"
)
Result of executing the above code:
changeset for schema with traditional embeds: embeds are still changesets
#Ecto.Changeset<
action: nil,
changes: %{
children: [
#Ecto.Changeset<
action: :insert,
changes: %{age: 10, name: "Tic"},
errors: [],
data: #ChildSchema<>,
valid?: true
>,
#Ecto.Changeset<
action: :insert,
changes: %{age: 12, name: "Tac"},
errors: [],
data: #ChildSchema<>,
valid?: true
>
]
},
errors: [],
data: #ParentSchema<>,
valid?: true
>
changeset for schema with polymorphic embeds: changes in embeds have already been applied
#Ecto.Changeset<
action: nil,
changes: %{
children: [
%ChildSchema{
id: "cd555105-4033-425f-b984-29f8c2062de9",
name: "Tic",
age: 10
},
%ChildSchema{
id: "5c55473d-e1d5-4f76-96a9-b63685c55ca3",
name: "Tac",
age: 12
}
]
},
errors: [],
data: #ParentSchemaWithPolymorphicChildren<>,
valid?: true
>
Hi Mathieu,
Thanks for creating this library, it's a great contribution! I have a few questions about the recommended way to store records with schemas that use this library in a database.
create table(:reminders) do
add :date, :utc_datetime
add :text, :string
add :address, :string
add :confirmed, :boolean
add :number, :string
...
channel
?Thanks! I'm fairly new to Elixir/Ecto, so maybe these question doesn't need to be answered for most users. If they do, I'd happy to make a PR to add explanation based on your answers to the docs.
Is there anything that needs to be done to get an array of items to be cast correctly by this function?
I've setup my fields as shown in the example in the README, https://github.com/mathieuprog/polymorphic_embed#list-of-polymorphic-embeds, and when attempting to do an equivalent to cast_polymorphic_embed(:contexts, required: true)
I get an equivalent error to protocol Enumerable not implemented for %Device{id: "xyz", value: 256} of type Device (a struct).
I currently have a schema with this shape:
schema "ClientInstance" do
field :ext_ref, Ecto.UUID
field :notes, :string
embeds_one :query, EmbeddedSchema.ClientInstance
timestamps()
end
with the embedded schema as follows:
embedded_schema do
field :includes, {:array, :string}
field :queries, {:array, PolymorphicEmbed},
types: [
queryquery: [module: Query, identify_by_fields: [:bar]],
...
],
on_type_not_found: :ignore,
on_replace: :delete
end
Given a changeset like so:
#Ecto.Changeset<
action: nil,
changes: %{
ext_ref: "6201d502-6422-4b49-9849-e07fd4fe5899",
notes: "Misbehaving Tester",
query: #Ecto.Changeset<
action: :insert,
changes: %{
queries: [
%Query{
foo: nil,
bar: "5642"
},
%Query{
foo: nil,
bar: "5672"
}
]
},
errors: [],
data: #EmbeddedSchema.ClientInstance<>,
valid?: true
>
},
errors: [],
data: #Controller.ClientInstance<>,
valid?: true
>
I have this rendering via polymorphic_embed_inputs_for/4 the form successfully against the existing data
<%= polymorphic_embed_inputs_for query_form, :queries, :queryquery, fn fp -> %>
, with the array of existing foo/bar fields present. On validation however, I only recieve a single one in the params passed back to the form as so:
%{
"notes" => "Misbehaving Tester.",
"query" => %{
"includes" => [""],
"queries" => %{
"__type__" => "queryquery",
"foo" => " ",
"bar" => "5672"
}
}
}
as opposed to an array like I was expecting
%{
"notes" => "Misbehaving Tester.",
"query" => %{
"includes" => [""],
"queries" => [
%{
"__type__" => "queryquery",
"foo" => " ",
"bar" => "5642"
},
%{
"__type__" => "queryquery",
"foo" => " ",
"bar" => "5672"
}]
}
}
I've been toying around with various solutions but I've reached a bit of an impasse at this time.
Ecto 3.10 introduces the sort_param
and drop_param
options for cast_assoc
and cast_embed
: https://hexdocs.pm/ecto/Ecto.Changeset.html#cast_assoc/3-sorting-and-deleting-from-many-collections. This is also showcased in a LiveView context here. I think it would be useful to add those options to cast_polymorphic_embed
as well.
When creating a form to edit a schema with a polymorphic embed using a custom type_field
, PolymorphicEmbed.HTML.Form.get_polymorphic_type
returns nil
regardless of the data shape.
I believe this is due to the hardcoded match for "__type__"/:__type__
keys when matching on the result of input_value(form, field)
.
This seems to be confirmed by adding a <%= hidden_input(f, :__type__, value: input_value(f, :actual_type_field) %>
to the form, which seems to make it work.
PolymorphicEmbed.HTML.Form.get_polymorphic_type
should match on thetype_field
value set in the schema.
Thanks for the work on this great library!
Hey, there. I would like to use it by next case:
I need to check a field for the presence of some types, if the type is different or all is missing, don't validate and set an empty map by default. Is it possible?
I would like to see:
schema "events" do
field :meta, PolymorphicEmbed,
types: [
g: Goal
],
on_replace: :update
timestamps()
end
%Event{
meta: %Goal{
assist: nil,
player: nil,
pre_assist: nil,
team: nil
},
type: "g"
}
%Event{
meta: %{},
type: "blablabla"
}
I found this, but it blocks recording to the database
:on_type_not_found – specify whether to raise or add a changeset error if the embed's type cannot be inferred. Possible values are :raise and :changeset_error. By default, a changeset error "is invalid" is added.
In my world, I would like to use something like on_type_not_found: :non_validation
Maybe I don’t know something, but what other ideas?
In the frontend, I would like to present the user the possibility to choose the type and then the correct polymorphic form will appear.
To get a list of availble types of a field, I was thinking about: https://hexdocs.pm/polymorphic_embed/PolymorphicEmbed.html#types/2
But I am getting an error, because the atoms does not exist:
iex(7)> PolymorphicEmbed.types(MySchema, :a_poly_field)
** (ArgumentError) errors were found at the given arguments:
* 1st argument: not an already existing atom
:erlang.binary_to_existing_atom("catalog", :utf8)
(polymorphic_embed 1.9.0) lib/polymorphic_embed.ex:344: anonymous fn/1 in PolymorphicEmbed.types/2
(elixir 1.13.4) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
(elixir 1.13.4) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
Is there a way around this? I understand String.to_atom
is dangerous, is there an alternative?
Thank you
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.