Skip to content

Commit 1eedc64

Browse files
authored
Merge pull request #376 from danschultzer/ecto-after-compile
Add after compile validation check to ensure all fields and assocs have been defined
2 parents 36a9112 + 510fbfd commit 1eedc64

File tree

4 files changed

+162
-2
lines changed

4 files changed

+162
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* [`Pow.Ecto.Schema.Changeset`] `Pow.Ecto.Schema.Changeset.confirm_password_changeset/3` now uses the `Ecto.Changeset.validate_confirmation/3` for validation and expects `:password_confirmation` instead of `:confirm_password` in params
1414
* [`Pow.Ecto.Schema.Changeset`] `Pow.Ecto.Schema.Changeset.new_password_changeset/3` now only requires the `:password_hash` if there have been no previous errors set in the changeset
1515
* [`Pow.Ecto.Schema`] No longer adds `:confirm_password` virtual field
16+
* [`Pow.Ecto.Schema`] Now has an `@after_compile` callback that ensures all required fields has been defined
1617
* [`PowInvitation.Phoenix.InvitationView`] Now renders `:password_confirmation` field instead of `:confirm_password`
1718
* [`PowResetPassword.Phoenix.ResetPasswordView`] Now renders `:password_confirmation` field instead of `:confirm_password`
1819
* [`Pow.Phoenix.RegistrationView`] Now renders `:password_confirmation` field instead of `:confirm_password`

lib/pow/ecto/schema.ex

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,25 @@ defmodule Pow.Ecto.Schema do
9494
end
9595
end
9696
97+
An `@after_compile` callback will raise an error if there are missing fields
98+
or associations, so you can forego the `pow_user_fields/0` call, and write
99+
out the whole schema instead:
100+
101+
defmodule MyApp.Users.User do
102+
use Ecto.Schema
103+
use Pow.Ecto.Schema, user_id_field: :email
104+
105+
schema "users" do
106+
field :email, :string, null: false
107+
field :password_hash, :string
108+
field :current_password, :string, virtual: true
109+
field :password, :string, virtual: true
110+
field :confirm_password, :string, virtual: true
111+
112+
timestamps()
113+
end
114+
end
115+
97116
## Customize Pow changeset
98117
99118
You can extract individual changeset methods to modify the changeset flow
@@ -124,6 +143,11 @@ defmodule Pow.Ecto.Schema do
124143
alias Ecto.Changeset
125144
alias Pow.Config
126145

146+
defmodule SchemaError do
147+
@moduledoc false
148+
defexception [:message]
149+
end
150+
127151
@callback changeset(Ecto.Schema.t() | Changeset.t(), map()) :: Changeset.t()
128152
@callback verify_password(Ecto.Schema.t(), binary()) :: boolean()
129153

@@ -145,6 +169,7 @@ defmodule Pow.Ecto.Schema do
145169
unquote(__MODULE__).__register_fields__()
146170
unquote(__MODULE__).__register_assocs__()
147171
unquote(__MODULE__).__register_user_id_field__()
172+
unquote(__MODULE__).__register_after_compile_validation__()
148173
end
149174
end
150175

@@ -294,6 +319,87 @@ defmodule Pow.Ecto.Schema do
294319
def user_id_field(config) when is_list(config), do: Config.get(config, :user_id_field, @default_user_id_field)
295320
def user_id_field(_any), do: @default_user_id_field
296321

322+
@doc false
323+
defmacro __register_after_compile_validation__ do
324+
quote do
325+
def pow_validate_after_compilation!(env, _bytecode) do
326+
unquote(__MODULE__).__require_assocs__(__MODULE__)
327+
unquote(__MODULE__).__require_fields__(__MODULE__)
328+
end
329+
330+
@after_compile {__MODULE__, :pow_validate_after_compilation!}
331+
end
332+
end
333+
334+
@doc false
335+
def __require_assocs__(module) do
336+
ecto_assocs = Module.get_attribute(module, :ecto_assocs)
337+
338+
module
339+
|> Module.get_attribute(:pow_assocs)
340+
|> Enum.reverse()
341+
|> Enum.filter(fn assoc ->
342+
not Enum.any?(ecto_assocs, &assocs_match?(elem(assoc, 0), elem(assoc, 1), &1))
343+
end)
344+
|> Enum.map(fn
345+
{type, name, queryable} -> "#{type} #{inspect name}, #{inspect queryable}"
346+
{type, name, queryable, defaults} -> "#{type} #{inspect name}, #{inspect queryable}, #{inspect defaults}"
347+
end)
348+
|> case do
349+
[] -> :ok
350+
assoc_defs -> raise_missing_assocs_error(module, assoc_defs)
351+
end
352+
end
353+
354+
defp raise_missing_assocs_error(module, assoc_defs) do
355+
raise SchemaError, message:
356+
"""
357+
Please define the following association(s) in the schema for #{inspect module}:
358+
359+
#{Enum.join(assoc_defs, "\n")}
360+
"""
361+
end
362+
363+
@doc false
364+
def __require_fields__(module) do
365+
ecto_fields = Module.get_attribute(module, :ecto_fields)
366+
changeset_fields = Module.get_attribute(module, :changeset_fields)
367+
368+
module
369+
|> Module.get_attribute(:pow_fields)
370+
|> Enum.reverse()
371+
|> Enum.filter(&missing_field?(&1, ecto_fields, changeset_fields))
372+
|> Enum.map(fn
373+
{name, type} -> "field #{inspect name}, #{inspect type}"
374+
{name, type, defaults} -> "field #{inspect name}, #{inspect type}, #{inspect defaults}"
375+
end)
376+
|> case do
377+
[] -> :ok
378+
field_defs -> raise_missing_fields_error(module, field_defs)
379+
end
380+
end
381+
382+
defp missing_field?({name, type, defaults}, ecto_fields, changeset_fields) do
383+
case defaults[:virtual] do
384+
true -> missing_field?(name, type, changeset_fields)
385+
_any -> missing_field?(name, type, ecto_fields)
386+
end
387+
end
388+
defp missing_field?({name, type}, ecto_fields, _changeset_fields),
389+
do: missing_field?(name, type, ecto_fields)
390+
defp missing_field?(name, type, existing_fields) do
391+
not Enum.member?(existing_fields, {name, type})
392+
end
393+
394+
defp raise_missing_fields_error(module, field_defs) do
395+
raise SchemaError, message:
396+
"""
397+
Please define the following field(s) in the schema for #{inspect module}:
398+
399+
#{Enum.join(field_defs, "\n")}
400+
"""
401+
end
402+
297403
@doc """
298404
Normalizes the user id field.
299405

lib/pow/extension/ecto/schema.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,11 @@ defmodule Pow.Extension.Ecto.Schema do
9999
@doc false
100100
defmacro __register_after_compile_validation__ do
101101
quote do
102-
def validate_after_compilation!(env, _bytecode) do
102+
def pow_extension_validate_after_compilation!(env, _bytecode) do
103103
unquote(__MODULE__).validate!(@pow_extension_config, __MODULE__)
104104
end
105105

106-
@after_compile {__MODULE__, :validate_after_compilation!}
106+
@after_compile {__MODULE__, :pow_extension_validate_after_compilation!}
107107
end
108108
end
109109

test/pow/ecto/schema_test.exs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,57 @@ defmodule Pow.Ecto.SchemaTest do
6868
assert %{on_replace: :mark_as_invalid} = OverrideAssocUser.__schema__(:association, :parent)
6969
assert %{on_delete: :delete_all} = OverrideAssocUser.__schema__(:association, :children)
7070
end
71+
72+
module_raised_with =
73+
try do
74+
defmodule MissingAssocsUser do
75+
use Ecto.Schema
76+
use Pow.Ecto.Schema
77+
78+
@pow_assocs {:belongs_to, :invited_by, __MODULE__, foreign_key: :user_id}
79+
@pow_assocs {:has_many, :invited, __MODULE__}
80+
81+
schema "users" do
82+
timestamps()
83+
end
84+
end
85+
rescue
86+
e in Pow.Ecto.Schema.SchemaError -> e.message
87+
end
88+
89+
test "requires assocs defined" do
90+
assert unquote(module_raised_with) ==
91+
"""
92+
Please define the following association(s) in the schema for Pow.Ecto.SchemaTest.MissingAssocsUser:
93+
94+
belongs_to :invited_by, Pow.Ecto.SchemaTest.MissingAssocsUser, [foreign_key: :user_id]
95+
has_many :invited, Pow.Ecto.SchemaTest.MissingAssocsUser
96+
"""
97+
end
98+
99+
module_raised_with =
100+
try do
101+
defmodule MissingFieldsUser do
102+
use Ecto.Schema
103+
use Pow.Ecto.Schema
104+
105+
schema "users" do
106+
timestamps()
107+
end
108+
end
109+
rescue
110+
e in Pow.Ecto.Schema.SchemaError -> e.message
111+
end
112+
113+
test "requires fields defined" do
114+
assert unquote(module_raised_with) ==
115+
"""
116+
Please define the following field(s) in the schema for Pow.Ecto.SchemaTest.MissingFieldsUser:
117+
118+
field :email, :string, [null: false]
119+
field :password_hash, :string
120+
field :current_password, :string, [virtual: true]
121+
field :password, :string, [virtual: true]
122+
"""
123+
end
71124
end

0 commit comments

Comments
 (0)