1
Justfile
|
|
@ -9,6 +9,7 @@ migrate-database:
|
|||
|
||||
reset-database:
|
||||
mix ash.reset
|
||||
MIX_ENV=test mix ash.setup
|
||||
|
carla marked this conversation as resolved
Outdated
|
||||
|
||||
seed-database:
|
||||
mix run priv/repo/seeds.exs
|
||||
|
|
|
|||
|
|
@ -14,6 +14,23 @@ defmodule Mv.Membership.Member do
|
|||
create :create_member do
|
||||
primary? true
|
||||
argument :properties, {:array, :map}
|
||||
|
||||
accept [
|
||||
:first_name,
|
||||
:last_name,
|
||||
:email,
|
||||
:birth_date,
|
||||
:paid,
|
||||
:phone_number,
|
||||
:join_date,
|
||||
:exit_date,
|
||||
:notes,
|
||||
:city,
|
||||
:street,
|
||||
:house_number,
|
||||
:postal_code
|
||||
]
|
||||
|
||||
change manage_relationship(:properties, type: :create)
|
||||
end
|
||||
|
||||
|
|
@ -21,12 +38,176 @@ defmodule Mv.Membership.Member do
|
|||
primary? true
|
||||
require_atomic? false
|
||||
|
carla marked this conversation as resolved
Outdated
rafael
commented
I think this will run using ash's "notifiers", and as such will run outside the transaction used for the destroy itself. This means that we could end up with errors if postgres detects that a property does not have an associated member. Also, I think the "references" block in the property's postgres configuration should take make this line obsolete because it tells postgres to cascade delete properties when a member is deleted? 🤔 I think this will run using ash's "notifiers", and as such [will run outside the transaction used for the destroy itself](https://hexdocs.pm/ash/notifiers.html#transactions). This means that we could end up with errors if postgres detects that a property does not have an associated member.
Also, I think the "references" block in the property's postgres configuration should take make this line obsolete because it tells postgres to cascade delete properties when a member is deleted? 🤔
moritz
commented
I tried different approaches to get the cascade deletion working. I think in the end the postgres reference block has done the job and this is still a leftover. I will try to run it without this change. I tried different approaches to get the cascade deletion working. I think in the end the postgres reference block has done the job and this is still a leftover. I will try to run it without this change.
carla
commented
Seems that properties are deleted using default :destroy with the reference block Seems that properties are deleted using default :destroy with the reference block
|
||||
argument :properties, {:array, :map}
|
||||
|
||||
accept [
|
||||
:first_name,
|
||||
:last_name,
|
||||
:email,
|
||||
:birth_date,
|
||||
:paid,
|
||||
:phone_number,
|
||||
:join_date,
|
||||
:exit_date,
|
||||
:notes,
|
||||
:city,
|
||||
:street,
|
||||
:house_number,
|
||||
:postal_code
|
||||
]
|
||||
|
||||
change manage_relationship(:properties, on_match: :update, on_no_match: :create)
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_v7_primary_key :id
|
||||
|
||||
attribute :first_name, :string do
|
||||
allow_nil? false
|
||||
constraints min_length: 1
|
||||
end
|
||||
|
||||
attribute :last_name, :string do
|
||||
allow_nil? false
|
||||
constraints min_length: 1
|
||||
end
|
||||
|
||||
attribute :email, :string do
|
||||
allow_nil? false
|
||||
constraints min_length: 5, max_length: 254
|
||||
end
|
||||
|
||||
attribute :birth_date, :date do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :paid, :boolean do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :phone_number, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
carla marked this conversation as resolved
Outdated
rafael
commented
I'm fine with doing this using custom validations as I think "real" code is easier to read than DSLs - but I think if we wanted to, we could achieve this in a more concise way using ash's built-in compare validation, e.g.: Not sure if I got the syntax exactly right, but I hope you get what I mean. I think this approach would work for other custom validations in this file as well, e.g. using the built-in One advantage of using the built-in validations is that they are "atomic", meaning they can run in the database directly, which can improve performance. I'm fine with doing this using custom validations as I think "real" code is easier to read than DSLs - but I think if we wanted to, we could achieve this in a more concise way using ash's [built-in compare validation](https://hexdocs.pm/ash/Ash.Resource.Validation.Builtins.html#compare/2), e.g.:
```elixir
validate compare(:join_date, less_than: &Date.utc_today/0),
where: [present(:join_date)],
message: "cannot be in the future"
```
Not sure if I got the syntax exactly right, but I hope you get what I mean. I think this approach would work for other custom validations in this file as well, e.g. using the built-in `match` validation for regexes.
One advantage of using the built-in validations is that they are "atomic", meaning they can run in the database directly, which can improve performance.
moritz
commented
Thank you for the hint, I will apply it and test if it works. Thank you for the hint, I will apply it and test if it works.
carla
commented
Maybe not from a technical but from a user perspective: Why should it not be possible to add members already, where the membership starts maybe beginning of next month? Should we take the question to the Pilotvereine? Maybe not from a technical but from a user perspective: Why should it not be possible to add members already, where the membership starts maybe beginning of next month? Should we take the question to the Pilotvereine?
|
||||
|
||||
attribute :join_date, :date do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :exit_date, :date do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :notes, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :city, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :street, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :house_number, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :postal_code, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
end
|
||||
|
||||
validations do
|
||||
# Required fields are covered by allow_nil? false
|
||||
|
||||
# First name and last name must not be empty
|
||||
validate present(:first_name)
|
||||
validate present(:last_name)
|
||||
validate present(:email)
|
||||
|
||||
# Birth date not in the future
|
||||
validate fn changeset, _ ->
|
||||
birth_date = Ash.Changeset.get_attribute(changeset, :birth_date)
|
||||
|
||||
if birth_date && Date.compare(birth_date, Date.utc_today()) == :gt do
|
||||
{:error, field: :birth_date, message: "cannot be in the future"}
|
||||
else
|
||||
|
carla marked this conversation as resolved
Outdated
rafael
commented
I don't think this works the way it appears: If I remove the
I don't think this works the way it appears: If I remove the `type="checkbox"` attribute in the paid form field, and e.g. enter "not a boolean" instead of "true" or "false", I get back an "is invalid" error. This hints to me that the validate function does not get called if the input does not cast to the required attribute type. There's a [hint buried in the ash docs](https://hexdocs.pm/ash/Ash.Resource.Validation.Builtins.html#absent/2):
> Keep in mind that some types cast certain values to nil, and validations are applied after all inputs have been cast. For example, a :string type attribute with the default constraints will cast "" as nil, meaning an input of "" would pass the absent validation.
carla
commented
Should we omit that validation? Because we define "paid" as boolean and I think using Should we omit that validation? Because we define "paid" as boolean and I think using `type="checkbox"` should not be removed anywhere or am I wrong?
rafael
commented
Just realized that my comment was very unspecific about what to change :D I think we can omit the validation, because ash already ensures that the attribute is a boolean, because the attribute definition marks it as the type Just realized that my comment was very unspecific about what to change :D I think we can omit the validation, because ash already ensures that the attribute is a boolean, because the attribute definition marks it as the type `:boolean`. We should not remove the `type="checkbox"`, that was just what I did to be able to send invalid data to the server.
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# Join date not in the future
|
||||
validate fn changeset, _ ->
|
||||
join_date = Ash.Changeset.get_attribute(changeset, :join_date)
|
||||
|
||||
if join_date && Date.compare(join_date, Date.utc_today()) == :gt do
|
||||
{:error, field: :join_date, message: "cannot be in the future"}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# Exit date not before join date
|
||||
validate fn changeset, _ ->
|
||||
join_date = Ash.Changeset.get_attribute(changeset, :join_date)
|
||||
exit_date = Ash.Changeset.get_attribute(changeset, :exit_date)
|
||||
|
||||
if join_date && exit_date && Date.compare(exit_date, join_date) == :lt do
|
||||
{:error, field: :exit_date, message: "cannot be before join date"}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# Phone number format (only if set)
|
||||
validate fn changeset, _ ->
|
||||
phone = Ash.Changeset.get_attribute(changeset, :phone_number)
|
||||
|
||||
if phone && !Regex.match?(~r/^\+?[0-9\- ]{6,20}$/, phone) do
|
||||
{:error, field: :phone_number, message: "is not a valid phone number"}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# Postal code format (only if set)
|
||||
validate fn changeset, _ ->
|
||||
postal_code = Ash.Changeset.get_attribute(changeset, :postal_code)
|
||||
|
||||
if postal_code && !Regex.match?(~r/^\d{5}$/, postal_code) do
|
||||
{:error, field: :postal_code, message: "must consist of 5 digits"}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# paid must be boolean if set
|
||||
validate fn changeset, _ ->
|
||||
paid = Ash.Changeset.get_attribute(changeset, :paid)
|
||||
|
||||
if not is_nil(paid) and not is_boolean(paid) do
|
||||
{:error, field: :paid, message: "must be true or false"}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# Email validation with EctoCommons.EmailValidator
|
||||
validate fn changeset, _ ->
|
||||
email = Ash.Changeset.get_attribute(changeset, :email)
|
||||
|
||||
changeset2 =
|
||||
{%{}, %{email: :string}}
|
||||
|> Ecto.Changeset.cast(%{email: email}, [:email])
|
||||
|> EctoCommons.EmailValidator.validate_email(:email, checks: [:html_input, :pow])
|
||||
|
||||
if changeset2.valid? do
|
||||
:ok
|
||||
else
|
||||
{:error, field: :email, message: "is not a valid email"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
relationships do
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ defmodule Mv.Membership.PropertyType do
|
|||
attribute :value_type, :atom,
|
||||
constraints: [one_of: [:string, :integer, :boolean, :date, :email]],
|
||||
allow_nil?: false,
|
||||
description: "Definies the datatype `Property.value` is interpreted as"
|
||||
description: "Defines the datatype `Property.value` is interpreted as"
|
||||
|
||||
attribute :description, :string, allow_nil?: true, public?: true
|
||||
|
||||
|
|
|
|||
3
mix.exs
|
|
@ -69,7 +69,8 @@ defmodule Mv.MixProject do
|
|||
{:bandit, "~> 1.5"},
|
||||
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
|
||||
{:sobelow, "~> 0.14", only: [:dev, :test], runtime: false},
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
{:ecto_commons, "~> 0.3"}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
4
mix.lock
|
|
@ -13,9 +13,11 @@
|
|||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
||||
"ecto": {:hex, :ecto, "3.12.6", "8bf762dc5b87d85b7aca7ad5fe31ef8142a84cea473a3381eb933bd925751300", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c0cba01795463eebbcd9e4b5ef53c1ee8e68b9c482baef2a80de5a61e7a57fe"},
|
||||
"ecto_commons": {:hex, :ecto_commons, "0.3.6", "7b1d9e59396cf8c8cbe5a26d50d03f9b6d0fe6c640210dd503622f276f1e59bb", [:mix], [{:burnex, "~> 3.0", [hex: :burnex, repo: "hexpm", optional: true]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_phone_number, "~> 0.2", [hex: :ex_phone_number, repo: "hexpm", optional: false]}, {:luhn, "~> 0.3.0", [hex: :luhn, repo: "hexpm", optional: false]}], "hexpm", "3f12981a1e398f206c5d2014e7b732b7ec91b110b9cb84875cb5b28fc75d7a0a"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
|
||||
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
|
||||
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
|
||||
"ex_phone_number": {:hex, :ex_phone_number, "0.4.5", "2065cc48c3e9d1ed9821f50877c32f2f6898362cb990f44147ca217c5d1374ed", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "67163f8706f8cbfef1b1f4b9230c461f19786d0d79fd0b22cbeeefc6f0b99d4a"},
|
||||
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
|
||||
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
|
||||
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
|
||||
|
|
@ -30,6 +32,7 @@
|
|||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
|
||||
"live_debugger": {:hex, :live_debugger, "0.2.4", "2e0b02874ca562ba2d8cebb9e024c25c0ae9c1f4ee499135a70814e1dea6183e", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20.4 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "bfd0db143be54ccf2872f15bfd2209fbec1083d0b06b81b4cedeecb2fa9ac208"},
|
||||
"luhn": {:hex, :luhn, "0.3.3", "5aa0c6a32c2db4b9db9f9b883ba8301c1ae169d57199b9e6cb1ba2707bc51d96", [:mix], [], "hexpm", "3e823a913a25aab51352c727f135278d22954874d5f0835be81ed4fec3daf78d"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"},
|
||||
|
|
@ -58,6 +61,7 @@
|
|||
"splode": {:hex, :splode, "0.2.9", "3a2776e187c82f42f5226b33b1220ccbff74f4bcc523dd4039c804caaa3ffdc7", [:mix], [], "hexpm", "8002b00c6e24f8bd1bcced3fbaa5c33346048047bb7e13d2f3ad428babbd95c3"},
|
||||
"stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},
|
||||
"swoosh": {:hex, :swoosh, "1.19.2", "b2325aa7cd2bcd63ba023fa07a73dfc4f80660a592d40912975a879966ed9b7b", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cab7ef7c2c94c68fe21d3da26f6b86db118fdf4e7024ccb5842a4972c1056837"},
|
||||
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
|
||||
"tailwind": {:hex, :tailwind, "0.3.1", "a89d2835c580748c7a975ad7dd3f2ea5e63216dc16d44f9df492fbd12c094bed", [:mix], [], "hexpm", "98a45febdf4a87bc26682e1171acdedd6317d0919953c353fcd1b4f9f4b676a2"},
|
||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
||||
|
|
|
|||
45
priv/repo/migrations/20250617090641_member_fields.exs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
defmodule Mv.Repo.Migrations.MemberFields do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:members) do
|
||||
add :first_name, :text, null: false
|
||||
add :last_name, :text, null: false
|
||||
add :email, :text, null: false
|
||||
add :birth_date, :date
|
||||
add :paid, :boolean
|
||||
add :phone_number, :text
|
||||
add :join_date, :date
|
||||
add :exit_date, :date
|
||||
add :notes, :text
|
||||
add :city, :text
|
||||
add :street, :text
|
||||
add :house_number, :text
|
||||
add :postal_code, :text
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:members) do
|
||||
remove :postal_code
|
||||
remove :house_number
|
||||
remove :street
|
||||
remove :city
|
||||
remove :notes
|
||||
remove :exit_date
|
||||
remove :join_date
|
||||
remove :phone_number
|
||||
remove :paid
|
||||
remove :birth_date
|
||||
remove :email
|
||||
remove :last_name
|
||||
remove :first_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -7,37 +7,37 @@ alias Mv.Membership
|
|||
|
||||
for attrs <- [
|
||||
%{
|
||||
name: "Vorname",
|
||||
name: "First Name",
|
||||
value_type: :string,
|
||||
description: "Vorname des Mitglieds",
|
||||
description: "Member's first name",
|
||||
immutable: true,
|
||||
required: true
|
||||
},
|
||||
%{
|
||||
name: "Nachname",
|
||||
name: "Last Name",
|
||||
value_type: :string,
|
||||
description: "Nachname des Mitglieds",
|
||||
description: "Member's last name",
|
||||
immutable: true,
|
||||
required: true
|
||||
},
|
||||
%{
|
||||
name: "Geburtsdatum",
|
||||
name: "Birth Date",
|
||||
value_type: :date,
|
||||
description: "Geburtsdatum des Mitglieds",
|
||||
description: "Member's birth date",
|
||||
immutable: true,
|
||||
required: true
|
||||
},
|
||||
%{
|
||||
name: "Bezahlt",
|
||||
name: "Paid",
|
||||
value_type: :boolean,
|
||||
description: "Status des Mitgliedsbeitrages des Mitglieds",
|
||||
description: "Member's payment status",
|
||||
immutable: true,
|
||||
required: true
|
||||
},
|
||||
%{
|
||||
name: "Email",
|
||||
value_type: :email,
|
||||
description: "Email-Adresse des Mitglieds",
|
||||
description: "Member's email address",
|
||||
immutable: true,
|
||||
required: true
|
||||
}
|
||||
|
|
|
|||
187
priv/resource_snapshots/repo/members/20250617090641.json
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"uuid_generate_v7()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "first_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "last_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "email",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "birth_date",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "paid",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "phone_number",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "join_date",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "exit_date",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "notes",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "city",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "street",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "house_number",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "postal_code",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "CF80317E7EE409618E08458B10EE122FF605640DDA8CD6000B433F1979614F5D",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.Mv.Repo",
|
||||
"schema": null,
|
||||
"table": "members"
|
||||
}
|
||||
110
test/membership/member_test.exs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
defmodule Mv.Membership.MemberTest do
|
||||
use Mv.DataCase, async: false
|
||||
alias Mv.Membership
|
||||
|
||||
describe "Fields and Validations" do
|
||||
@valid_attrs %{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
birth_date: ~D[1990-01-01],
|
||||
paid: true,
|
||||
email: "john@example.com",
|
||||
phone_number: "+49123456789",
|
||||
join_date: ~D[2020-01-01],
|
||||
exit_date: nil,
|
||||
notes: "Test note",
|
||||
city: "Berlin",
|
||||
street: "Main Street",
|
||||
house_number: "1A",
|
||||
postal_code: "12345"
|
||||
}
|
||||
|
||||
test "First name is required and must not be empty" do
|
||||
attrs = Map.put(@valid_attrs, :first_name, "")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :first_name) =~ "must be present"
|
||||
end
|
||||
|
||||
test "Last name is required and must not be empty" do
|
||||
attrs = Map.put(@valid_attrs, :last_name, "")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :last_name) =~ "must be present"
|
||||
end
|
||||
|
||||
test "Email is required" do
|
||||
attrs = Map.put(@valid_attrs, :email, "")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :email) =~ "must be present"
|
||||
end
|
||||
|
||||
test "Email must be valid" do
|
||||
attrs = Map.put(@valid_attrs, :email, "test@")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :email) =~ "is not a valid email"
|
||||
end
|
||||
|
||||
test "Birth date is optional but must not be in the future" do
|
||||
attrs = Map.put(@valid_attrs, :birth_date, Date.utc_today() |> Date.add(1))
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :birth_date) =~ "cannot be in the future"
|
||||
end
|
||||
|
||||
test "Paid is optional but must be boolean if specified" do
|
||||
attrs = Map.put(@valid_attrs, :paid, nil)
|
||||
attrs2 = Map.put(@valid_attrs, :paid, "yes")
|
||||
assert {:ok, _member} = Membership.create_member(Map.delete(attrs, :paid))
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs2)
|
||||
assert error_message(errors, :paid) =~ "is invalid"
|
||||
end
|
||||
|
||||
test "Phone number is optional but must have a valid format if specified" do
|
||||
attrs = Map.put(@valid_attrs, :phone_number, "abc")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :phone_number) =~ "is not a valid phone number"
|
||||
attrs2 = Map.delete(@valid_attrs, :phone_number)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2)
|
||||
end
|
||||
|
||||
test "Join date is optional but must not be in the future" do
|
||||
attrs = Map.put(@valid_attrs, :join_date, Date.utc_today() |> Date.add(1))
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :join_date) =~ "cannot be in the future"
|
||||
attrs2 = Map.delete(@valid_attrs, :join_date)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2)
|
||||
end
|
||||
|
||||
test "Exit date is optional but must not be before join date if both are specified" do
|
||||
attrs = Map.put(@valid_attrs, :exit_date, ~D[2010-01-01])
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :exit_date) =~ "cannot be before join date"
|
||||
attrs2 = Map.delete(@valid_attrs, :exit_date)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2)
|
||||
end
|
||||
|
||||
test "Notes is optional" do
|
||||
attrs = Map.delete(@valid_attrs, :notes)
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
end
|
||||
|
||||
test "City, street, house number are optional" do
|
||||
attrs = @valid_attrs |> Map.drop([:city, :street, :house_number])
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
end
|
||||
|
||||
test "Postal code is optional but must have 5 digits if specified" do
|
||||
attrs = Map.put(@valid_attrs, :postal_code, "1234")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :postal_code) =~ "must consist of 5 digits"
|
||||
attrs2 = Map.delete(@valid_attrs, :postal_code)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2)
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function for error evaluation
|
||||
defp error_message(errors, field) do
|
||||
errors
|
||||
|> Enum.filter(fn err -> Map.get(err, :field) == field end)
|
||||
|> Enum.map(&Map.get(&1, :message, ""))
|
||||
|> List.first()
|
||||
end
|
||||
end
|
||||
Why not use
ash.resethere as well?typo that works as well. but i change it