Giter VIP home page Giter VIP logo

polymorphic_embed's People

Contributors

aaronrenner avatar atomkirk avatar daveli avatar davorbadrov avatar elmseld avatar gcauchon avatar gkdp avatar jmnsf avatar kirillrogovoy avatar maennchen avatar mathieuprog avatar onigirijack avatar simonprev avatar steffende avatar tomkonidas avatar woylie avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

polymorphic_embed's Issues

Type field not populated on cast

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.

During form submission, embedded arrays only contain the last entry

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.

How to define the tables/migrations?

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?

Regression for Form

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
>

[Advice] Using this library with insert_all

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!

Option to change the `__type__` key

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!

Hybrid matching strategy

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:

  • there are "type fields" in some of the models, but they are not always unique within all subtypes of a single type
  • many subtypes have unique sets of fields, but not all of them

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.

Trying to get a list of types for a field

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

Polymorphic embeds are not kept as changesets but are turned into structs - as opposed to what happens with ordinary embeds

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
>

Error occured when using with mongodb_ecto

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

Won't compile without a dependency on phoenix_html

$ 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.

Compile time dependencies between embedded schemas

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?

Can't load embed with ecto_sqlite3 adapter

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

polymorphic embed doesnt run embed`s changeset

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)

No changelog, lots of churn

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?

Missing/Not found type raising error when passed as string

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.

Could not infer polymorphic embed on the README example

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:

  • could not infer polymorphic embed from data %{<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!

Sourcing type from other column

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.

Question on DB schemas

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.

  • Would all fields from all embedded schemas (in one standard schema) be included in one table? So using your example from the readme, would the migration look like the following?
create table(:reminders) do
  add :date, :utc_datetime
  add :text, :string
  add :address, :string
  add :confirmed, :boolean
  add :number, :string
  ...
  • Would you add a string field for the channel?
  • If a field is shared by, for example, 2 of three of the options for an embedded schema, is it ok to share the column?

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.

PolymorphicEmbed.HTML.Form.get_polymorphic_type doesn't respect `type_field` option

Observed behavior

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.

Expected behavior

PolymorphicEmbed.HTML.Form.get_polymorphic_type should match on thetype_field value set in the schema.

Thanks for the work on this great library!

PolymorphicEmbed.HTML.Form.polymorphic_embed_inputs_for doesn't add arrray index to input field's name for a list of embeds

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!

Plans for this Library

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.

Question regarding cast_polymorphic_embed/3 and arrays

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).

Does not dump correctly with ecto_sqlite3

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.

PolymorphicEmbed.types not working if the module doesn't get recompiled

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?

Primary key for many.

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?

Using a factory library to seed schema fails when polymorphic field is nil

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

Changeset doesn't consider existing data

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

How is one supposed to display the polymorphic embed fields in a form if the schema is empty?

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.

`traverse_errors/2` blows up when the polymorphic embed is valid

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.

rendering the different types with `polymorphic_embed_inputs_for`

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.

non validation type

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?

Missing actual usage example

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.

Support for a map fallback when type is not found

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

Cannot change polymorhpic type on existing record

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

Validation messages for nested embeds

πŸ‘‹ 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?

CI pipeline

Would you mind if I opened a PR to add a workflow config for Github actions?

Casting Runtime errors

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?

traverse_errors/2 doesn't seem to work when run on a top-level schema which has_many children schemas that have polymorphic embeds

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.

Allow passing more args to embedded's changesets

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.

Unable to use it with ex_machina?

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.)

`put_polymorphic_embed` Support?

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?

Add support to `traverse_errors`

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]

https://github.com/elixir-ecto/ecto/blob/1c415141332f4c7df7fd1a8fbbc3b715668ae15b/lib/ecto/changeset.ex#L2937-L2966

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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.