-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add ability to configure the behavior of writing to a non-writable field #4734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
2351cc3
9dcc1ab
acde79b
412faf2
bdb8c69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -544,7 +544,8 @@ defmodule Ecto.Schema do | |
| :where, | ||
| :references, | ||
| :skip_default_validation, | ||
| :writable | ||
| :writable, | ||
| :on_writable_violation | ||
| ] | ||
|
|
||
| @doc """ | ||
|
|
@@ -700,6 +701,13 @@ defmodule Ecto.Schema do | |
| be further modified, even in an upsert. If set to `:never`, the field becomes | ||
| read only. Defaults to `:always`. | ||
|
|
||
| * `:on_writable_violation` - Defines what action to take when performing an insert or update | ||
| attempts to modify a field that should not be modified according to it's `:writable` value. | ||
| Must be one of `:nothing`, `:warn`, or `:raise`. If set to `:nothing`, the modification is | ||
| silently ignored. If set to `:warn`, the modification is ignored and a warning is logged. If set | ||
| to `:raise`, an exception is raised and the operation is aborted. If `:writable` is set to `:always`, | ||
| `:on_writable_violation` must be set to `:nothing`. Defaults to `:nothing`. | ||
|
Comment on lines
+705
to
+710
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added this as "the path of least resistance" to illustrate the functionality, but I am not confident this is the desired way forward as to the public facing API of how to configure this behavior. Some other options:
Open to suggestions :) |
||
|
|
||
| """ | ||
| defmacro field(name, type \\ :string, opts \\ []) do | ||
| quote do | ||
|
|
@@ -2013,6 +2021,7 @@ defmodule Ecto.Schema do | |
| virtual? = opts[:virtual] || false | ||
| pk? = opts[:primary_key] || false | ||
| writable = opts[:writable] || :always | ||
| on_writable_violation = opts[:on_writable_violation] || :nothing | ||
| put_struct_field(mod, name, Keyword.get(opts, :default)) | ||
|
|
||
| redact_field? = | ||
|
|
@@ -2069,6 +2078,10 @@ defmodule Ecto.Schema do | |
| raise ArgumentError, "autogenerated fields must always be writable" | ||
| end | ||
|
|
||
| if writable == :always and on_writable_violation != :nothing do | ||
| raise ArgumentError, "on_writable_violation must be :nothing for always writable fields" | ||
| end | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably don't need this check. And maybe users want to set |
||
| if pk? do | ||
| Module.put_attribute(mod, :ecto_primary_keys, name) | ||
| end | ||
|
|
@@ -2077,7 +2090,7 @@ defmodule Ecto.Schema do | |
| Module.put_attribute(mod, :ecto_query_fields, {name, type}) | ||
| end | ||
|
|
||
| Module.put_attribute(mod, :ecto_fields, {name, {type, writable}}) | ||
| Module.put_attribute(mod, :ecto_fields, {name, {type, {writable, on_writable_violation}}}) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should not need to change this now.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might be missing something - doesn't this need to be included here so that I can reference use the value to populate the data for the Or are you suggesting to separately populate something like I put it here so it was next to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, to use a separate attribute so we don't "pollute" all other fields to store this information. |
||
| end | ||
| end | ||
|
|
||
|
|
@@ -2383,7 +2396,7 @@ defmodule Ecto.Schema do | |
| end | ||
|
|
||
| load = | ||
| for {name, {type, _writable}} <- fields do | ||
| for {name, {type, {_writable, _on_writable_violation}}} <- fields do | ||
| if alias = field_sources[name] do | ||
| {name, {:source, alias, type}} | ||
| else | ||
|
|
@@ -2392,17 +2405,17 @@ defmodule Ecto.Schema do | |
| end | ||
|
|
||
| dump = | ||
| for {name, {type, writable}} <- fields do | ||
| for {name, {type, {writable, _on_writable_violation}}} <- fields do | ||
| {name, {field_sources[name] || name, type, writable}} | ||
| end | ||
|
|
||
| field_sources_quoted = | ||
| for {name, {_type, _writable}} <- fields do | ||
| for {name, {_type, {_writable, _on_writable_violation}}} <- fields do | ||
| {[:field_source, name], field_sources[name] || name} | ||
| end | ||
|
|
||
| types_quoted = | ||
| for {name, {type, _writable}} <- fields do | ||
| for {name, {type, {_writable, _on_writable_violation}}} <- fields do | ||
| {[:type, name], Macro.escape(type)} | ||
| end | ||
|
|
||
|
|
@@ -2426,7 +2439,7 @@ defmodule Ecto.Schema do | |
| embed_names = Enum.map(embeds, &elem(&1, 0)) | ||
|
|
||
| updatable = | ||
| for {name, {_, writable}} <- fields, reduce: {[], []} do | ||
| for {name, {_, {writable, _on_writable_violation}}} <- fields, reduce: {[], []} do | ||
| {keep, drop} -> | ||
| case writable do | ||
| :always -> {[name | keep], drop} | ||
|
|
@@ -2435,21 +2448,27 @@ defmodule Ecto.Schema do | |
| end | ||
|
|
||
| insertable = | ||
| for {name, {_, writable}} <- fields, reduce: {[], []} do | ||
| for {name, {_, {writable, _on_writable_violation}}} <- fields, reduce: {[], []} do | ||
| {keep, drop} -> | ||
| case writable do | ||
| :never -> {keep, [name | drop]} | ||
| _ -> {[name | keep], drop} | ||
| end | ||
| end | ||
|
|
||
| on_writable_violation = | ||
| for {name, {_, {_writable, on_writable_violation}}} <- fields do | ||
| {name, on_writable_violation} | ||
| end | ||
|
|
||
| single_arg = [ | ||
| {[:dump], dump |> Map.new() |> Macro.escape()}, | ||
| {[:load], load |> Macro.escape()}, | ||
| {[:associations], assoc_names}, | ||
| {[:embeds], embed_names}, | ||
| {[:updatable_fields], updatable}, | ||
| {[:insertable_fields], insertable}, | ||
| {[:on_writable_violation], on_writable_violation |> Map.new() |> Macro.escape()}, | ||
| {[:redact_fields], redacted_fields}, | ||
| {[:autogenerate_fields], Enum.flat_map(autogenerate, &elem(&1, 0))}, | ||
| {[:virtual_fields], Enum.map(virtual_fields, &elem(&1, 0))}, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can accidentally ignore a field being written to nil. The best is probably:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤦 Thank you for catching this! Will fix 👍