From e7d93e28509463d4ed5ead3ce2aca5b9858ca5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Wed, 28 May 2025 15:39:48 +0200 Subject: [PATCH 01/12] Replace accidental podman command with docker --- Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Justfile b/Justfile index ed412c3..b3541fd 100644 --- a/Justfile +++ b/Justfile @@ -36,4 +36,4 @@ build-docker-container: # This is meant for debugging the container build process only. run-docker-container: build-docker-container - podman run -e "SECRET_KEY_BASE=ahK8BeiDaibaige1ahkooS0chie9lo7the7uuzar0eeBeeCh2iereteshee2Oosu" -e='DATABASE_URL=postgres://postgres@localhost:5432/mv_dev' -e='PORT=4040' -e='PHX_HOST=localhost' --network=host mitgliederverwaltung \ No newline at end of file + docker run -e "SECRET_KEY_BASE=ahK8BeiDaibaige1ahkooS0chie9lo7the7uuzar0eeBeeCh2iereteshee2Oosu" -e='DATABASE_URL=postgres://postgres@localhost:5432/mv_dev' -e='PORT=4040' -e='PHX_HOST=localhost' --network=host mitgliederverwaltung \ No newline at end of file From feb747bd2130ba77952505a1844827c7dbbf84d5 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 22 May 2025 01:57:01 +0200 Subject: [PATCH 02/12] chore: add regen_migrations script and seed-database to Justfile --- Justfile | 7 +++++-- regen_migrations.sh | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100755 regen_migrations.sh diff --git a/Justfile b/Justfile index ed412c3..ffbc2cc 100644 --- a/Justfile +++ b/Justfile @@ -1,4 +1,4 @@ -run: install-dependencies start-database migrate-database +run: install-dependencies start-database migrate-database seed-database mix phx.server install-dependencies: @@ -10,6 +10,9 @@ migrate-database: reset-database: mix ash.reset +seed-database: + mix run priv/repo/seeds.exs + start-database: docker compose up -d @@ -36,4 +39,4 @@ build-docker-container: # This is meant for debugging the container build process only. run-docker-container: build-docker-container - podman run -e "SECRET_KEY_BASE=ahK8BeiDaibaige1ahkooS0chie9lo7the7uuzar0eeBeeCh2iereteshee2Oosu" -e='DATABASE_URL=postgres://postgres@localhost:5432/mv_dev' -e='PORT=4040' -e='PHX_HOST=localhost' --network=host mitgliederverwaltung \ No newline at end of file + podman run -e "SECRET_KEY_BASE=ahK8BeiDaibaige1ahkooS0chie9lo7the7uuzar0eeBeeCh2iereteshee2Oosu" -e='DATABASE_URL=postgres://postgres@localhost:5432/mv_dev' -e='PORT=4040' -e='PHX_HOST=localhost' --network=host mitgliederverwaltung diff --git a/regen_migrations.sh b/regen_migrations.sh new file mode 100755 index 0000000..34cd4b0 --- /dev/null +++ b/regen_migrations.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Get count of untracked migrations +N_MIGRATIONS=$(git ls-files --others priv/repo/migrations | wc -l) + +# Rollback untracked migrations +mix ash_postgres.rollback -n $N_MIGRATIONS + +# Delete untracked migrations and snapshots +git ls-files --others priv/repo/migrations | xargs rm +git ls-files --others priv/resource_snapshots | xargs rm + +# Regenerate migrations +mix ash.codegen --name $1 + +# Run migrations if flag +if echo $* | grep -e "-m" -q; then + mix ash.migrate +fi From 6cce36b26ededf009e65955efaa26e9e4c27d3c4 Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 28 May 2025 18:39:08 +0200 Subject: [PATCH 03/12] property value as Union type --- Justfile | 17 +++++++++++++ lib/membership/property.ex | 16 ++++++++++-- lib/membership/property_type.ex | 5 ++-- lib/mv_web/member_live/form_component.ex | 25 +++++++++++++++++-- ...s => 20250528163901_initial_migration.exs} | 4 +-- priv/repo/seeds.exs | 25 +++++++++++++++++-- ...0250514151922.json => 20250528163901.json} | 2 +- ...0250514151922.json => 20250528163901.json} | 4 +-- ...0250514151922.json => 20250528163901.json} | 4 +-- regen_migrations.sh | 19 -------------- 10 files changed, 87 insertions(+), 34 deletions(-) rename priv/repo/migrations/{20250514151922_initial_migration.exs => 20250528163901_initial_migration.exs} (96%) rename priv/resource_snapshots/repo/members/{20250514151922.json => 20250528163901.json} (87%) rename priv/resource_snapshots/repo/properties/{20250514151922.json => 20250528163901.json} (95%) rename priv/resource_snapshots/repo/property_types/{20250514151922.json => 20250528163901.json} (94%) delete mode 100755 regen_migrations.sh diff --git a/Justfile b/Justfile index ffbc2cc..aed4d09 100644 --- a/Justfile +++ b/Justfile @@ -40,3 +40,20 @@ build-docker-container: # This is meant for debugging the container build process only. run-docker-container: build-docker-container podman run -e "SECRET_KEY_BASE=ahK8BeiDaibaige1ahkooS0chie9lo7the7uuzar0eeBeeCh2iereteshee2Oosu" -e='DATABASE_URL=postgres://postgres@localhost:5432/mv_dev' -e='PORT=4040' -e='PHX_HOST=localhost' --network=host mitgliederverwaltung + +regen-migrations migration_name: + #!/bin/bash + set -euo pipefail + # Get count of untracked migrations + N_MIGRATIONS=$(git ls-files --others priv/repo/migrations | wc -l) + # Rollback untracked migrations + mix ash_postgres.rollback -n $N_MIGRATIONS + # Delete untracked migrations and snapshots + git ls-files --others priv/repo/migrations | xargs rm + git ls-files --others priv/resource_snapshots | xargs rm + # Regenerate migrations + mix ash.codegen --name {{migration_name}} + # Run migrations if flag + if echo $* | grep -e "-m" -q; then + mix ash.migrate + fi diff --git a/lib/membership/property.ex b/lib/membership/property.ex index 433fc63..4e96731 100644 --- a/lib/membership/property.ex +++ b/lib/membership/property.ex @@ -16,8 +16,16 @@ defmodule Mv.Membership.Property do attributes do uuid_primary_key :id - attribute :value, :string, - description: "Speichert den Wert, Typ-Interpretation per property_type.typ" + attribute :value, :union, + constraints: [ + storage: :type_and_value, + types: [ + boolean: [type: :boolean], + date: [type: :date], + integer: [type: :integer], + string: [type: :string] + ] + ] end relationships do @@ -25,4 +33,8 @@ defmodule Mv.Membership.Property do belongs_to :property_type, Mv.Membership.PropertyType end + + calculations do + calculate :value_to_string, :string, expr(value[:value] <> "") + end end diff --git a/lib/membership/property_type.ex b/lib/membership/property_type.ex index 560f679..9bf16c9 100644 --- a/lib/membership/property_type.ex +++ b/lib/membership/property_type.ex @@ -10,7 +10,7 @@ defmodule Mv.Membership.PropertyType do actions do defaults [:create, :read, :update, :destroy] - default_accept [:name, :type, :description, :immutable, :required] + default_accept [:name, :value_type, :description, :immutable, :required] end attributes do @@ -18,7 +18,8 @@ defmodule Mv.Membership.PropertyType do attribute :name, :string, allow_nil?: false, public?: true - attribute :type, :string, + attribute :value_type, :atom, + constraints: [one_of: [:string, :integer, :boolean, :date]], allow_nil?: false, description: "Definies the datatype `Property.value` is interpreted as" diff --git a/lib/mv_web/member_live/form_component.ex b/lib/mv_web/member_live/form_component.ex index b91d3c0..9db46b0 100644 --- a/lib/mv_web/member_live/form_component.ex +++ b/lib/mv_web/member_live/form_component.ex @@ -9,7 +9,11 @@ defmodule MvWeb.MemberLive.FormComponent do Enum.map(property_types, fn pt -> %{ "property_type_id" => pt.id, - "value" => nil + "value" => %{ + "type" => pt.value_type, + "value" => nil, + "_union_type" => Atom.to_string(pt.value_type) + } } end) @@ -34,7 +38,9 @@ defmodule MvWeb.MemberLive.FormComponent do > <.inputs_for :let={f_property} field={@form[:properties]}> <% type = Enum.find(@property_types, &(&1.id == f_property[:property_type_id].value)) %> - <.input field={f_property[:value]} label={type && type.name} /> + <.inputs_for :let={value_form} field={f_property[:value]}> + <.input field={value_form[:value]} label={type && type.name} /> + + Enum.map(member.properties, fn prop -> + %{ + "property_type_id" => prop.property_type_id, + "value" => %{ + "_union_type" => Atom.to_string(prop.value.type), + "type" => prop.value.type, + "value" => prop.value.value + } + } + end) + } + form = AshPhoenix.Form.for_update( member, :update_member, api: Mv.Membership, as: "member", + params: params, forms: [auto?: true] ) diff --git a/priv/repo/migrations/20250514151922_initial_migration.exs b/priv/repo/migrations/20250528163901_initial_migration.exs similarity index 96% rename from priv/repo/migrations/20250514151922_initial_migration.exs rename to priv/repo/migrations/20250528163901_initial_migration.exs index 64915c2..bf36e43 100644 --- a/priv/repo/migrations/20250514151922_initial_migration.exs +++ b/priv/repo/migrations/20250528163901_initial_migration.exs @@ -11,7 +11,7 @@ defmodule Mv.Repo.Migrations.InitialMigration do create table(:property_types, primary_key: false) do add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true add :name, :text, null: false - add :type, :text, null: false + add :value_type, :text, null: false add :description, :text add :immutable, :boolean, null: false, default: false add :required, :boolean, null: false, default: false @@ -21,7 +21,7 @@ defmodule Mv.Repo.Migrations.InitialMigration do create table(:properties, primary_key: false) do add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true - add :value, :text + add :value, :map add :member_id, :uuid add :property_type_id, :uuid end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 8fca144..27daf36 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -15,14 +15,35 @@ alias Mv.Membership for attrs <- [ %{ name: "Vorname", - type: "string", + value_type: :string, description: "Vorname des Mitglieds", immutable: true, required: true }, + %{ + name: "Nachname", + value_type: :string, + description: "Nachname des Mitglieds", + immutable: true, + required: true + }, + %{ + name: "Geburtsdatum", + value_type: :date, + description: "Geburtsdatum des Mitglieds", + immutable: true, + required: true + }, + %{ + name: "Bezahlt", + value_type: :boolean, + description: "Status des Mitgliedsbeitrages des Mitglieds", + immutable: true, + required: true + }, %{ name: "Email", - type: "string", + value_type: :string, description: "Email-Adresse des Mitglieds", immutable: true, required: true diff --git a/priv/resource_snapshots/repo/members/20250514151922.json b/priv/resource_snapshots/repo/members/20250528163901.json similarity index 87% rename from priv/resource_snapshots/repo/members/20250514151922.json rename to priv/resource_snapshots/repo/members/20250528163901.json index df811d8..2d08b17 100644 --- a/priv/resource_snapshots/repo/members/20250514151922.json +++ b/priv/resource_snapshots/repo/members/20250528163901.json @@ -16,7 +16,7 @@ "custom_indexes": [], "custom_statements": [], "has_create_action": true, - "hash": "A0402269CB456075B81CA4CB3A2135A2C88D8B7FD51CD7A23084AA5264FEE344", + "hash": "35D45214D6D344B0AF6CFCB69B8682FCB3D382D85883D3D3AAC1AEE7F54FD89A", "identities": [], "multitenancy": { "attribute": null, diff --git a/priv/resource_snapshots/repo/properties/20250514151922.json b/priv/resource_snapshots/repo/properties/20250528163901.json similarity index 95% rename from priv/resource_snapshots/repo/properties/20250514151922.json rename to priv/resource_snapshots/repo/properties/20250528163901.json index 0707bcf..daf3222 100644 --- a/priv/resource_snapshots/repo/properties/20250514151922.json +++ b/priv/resource_snapshots/repo/properties/20250528163901.json @@ -18,7 +18,7 @@ "references": null, "size": null, "source": "value", - "type": "text" + "type": "map" }, { "allow_nil?": true, @@ -84,7 +84,7 @@ "custom_indexes": [], "custom_statements": [], "has_create_action": true, - "hash": "F2A42A3427E0428637F465E4F357A3BE21B33231F94CF77B4843084128F6BDA5", + "hash": "8CF241CB9E8239511914EDEC96186BB7879529372BD8A4162431CCE9961F4F1B", "identities": [], "multitenancy": { "attribute": null, diff --git a/priv/resource_snapshots/repo/property_types/20250514151922.json b/priv/resource_snapshots/repo/property_types/20250528163901.json similarity index 94% rename from priv/resource_snapshots/repo/property_types/20250514151922.json rename to priv/resource_snapshots/repo/property_types/20250528163901.json index 5da5fa8..55f066a 100644 --- a/priv/resource_snapshots/repo/property_types/20250514151922.json +++ b/priv/resource_snapshots/repo/property_types/20250528163901.json @@ -27,7 +27,7 @@ "primary_key?": false, "references": null, "size": null, - "source": "type", + "source": "value_type", "type": "text" }, { @@ -66,7 +66,7 @@ "custom_indexes": [], "custom_statements": [], "has_create_action": true, - "hash": "47210108DE1E7B2A20A67205E875B3440526941E61AB95B166976E8CD8AA0955", + "hash": "F98A723AE0D20005FBE4205E46ABEE09A88DFF9334C85BADC1FBEEF100F3E25B", "identities": [ { "all_tenants?": false, diff --git a/regen_migrations.sh b/regen_migrations.sh deleted file mode 100755 index 34cd4b0..0000000 --- a/regen_migrations.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Get count of untracked migrations -N_MIGRATIONS=$(git ls-files --others priv/repo/migrations | wc -l) - -# Rollback untracked migrations -mix ash_postgres.rollback -n $N_MIGRATIONS - -# Delete untracked migrations and snapshots -git ls-files --others priv/repo/migrations | xargs rm -git ls-files --others priv/resource_snapshots | xargs rm - -# Regenerate migrations -mix ash.codegen --name $1 - -# Run migrations if flag -if echo $* | grep -e "-m" -q; then - mix ash.migrate -fi From 8f14224eb18c8cb2e22a1321718c45cf7f8edb77 Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 28 May 2025 19:02:42 +0200 Subject: [PATCH 04/12] choose input filed type by value_type --- lib/mv_web/member_live/form_component.ex | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/mv_web/member_live/form_component.ex b/lib/mv_web/member_live/form_component.ex index 9db46b0..101cf6c 100644 --- a/lib/mv_web/member_live/form_component.ex +++ b/lib/mv_web/member_live/form_component.ex @@ -39,7 +39,13 @@ defmodule MvWeb.MemberLive.FormComponent do <.inputs_for :let={f_property} field={@form[:properties]}> <% type = Enum.find(@property_types, &(&1.id == f_property[:property_type_id].value)) %> <.inputs_for :let={value_form} field={f_property[:value]}> - <.input field={value_form[:value]} label={type && type.name} /> + <% input_type = + cond do + type && type.value_type == :boolean -> "checkbox" + type && type.value_type == :date -> :date + true -> :text + end %> + <.input field={value_form[:value]} label={type && type.name} type={input_type} /> Date: Wed, 28 May 2025 19:51:34 +0200 Subject: [PATCH 05/12] chore(Justfile): allow regenerating migrations by commit hash --- Justfile | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/Justfile b/Justfile index aed4d09..05002d2 100644 --- a/Justfile +++ b/Justfile @@ -41,19 +41,33 @@ build-docker-container: run-docker-container: build-docker-container podman run -e "SECRET_KEY_BASE=ahK8BeiDaibaige1ahkooS0chie9lo7the7uuzar0eeBeeCh2iereteshee2Oosu" -e='DATABASE_URL=postgres://postgres@localhost:5432/mv_dev' -e='PORT=4040' -e='PHX_HOST=localhost' --network=host mitgliederverwaltung -regen-migrations migration_name: - #!/bin/bash +# Usage: +# just regen-migrations migration_name [commit_hash] +# If commit_hash is given, rollback & delete the migrations from that commit. +# Otherwise, rollback & delete all untracked migrations. +regen-migrations migration_name commit_hash='': + #!/usr/bin/env bash set -euo pipefail - # Get count of untracked migrations - N_MIGRATIONS=$(git ls-files --others priv/repo/migrations | wc -l) - # Rollback untracked migrations - mix ash_postgres.rollback -n $N_MIGRATIONS - # Delete untracked migrations and snapshots - git ls-files --others priv/repo/migrations | xargs rm - git ls-files --others priv/resource_snapshots | xargs rm - # Regenerate migrations - mix ash.codegen --name {{migration_name}} - # Run migrations if flag - if echo $* | grep -e "-m" -q; then - mix ash.migrate + # Pick migrations either from the given commit or untracked files + if [ -n "{{commit_hash}}" ]; then + echo "→ Rolling back migrations from commit {{commit_hash}}" + MIG_FILES=$(git show --name-only --pretty=format: "{{commit_hash}}" \ + | grep -E "^priv/repo/migrations/|^priv/resource_snapshots") + else + echo "→ Rolling back all untracked migrations" + MIG_FILES=$(git ls-files --others priv/repo/migrations) fi + + # Roll back in Ash + COUNT=$(echo "$MIG_FILES" | wc -l) + mix ash_postgres.rollback -n "$COUNT" + + # Remove the migration files + echo removing $MIG_FILES + echo "$MIG_FILES" | xargs rm -f + + # Also clean up any untracked resource snapshots + git ls-files --others priv/resource_snapshots | xargs rm -f + + # Generate a fresh migration + mix ash.codegen --name "{{migration_name}}" From 6a22efce1a921ea49ce5b37028d98dcb3d956a2a Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 28 May 2025 22:04:54 +0200 Subject: [PATCH 06/12] feat: add custom email type for validation --- lib/membership/email.ex | 34 +++++++++++++++++++++++++++++++++ lib/membership/property.ex | 3 ++- lib/membership/property_type.ex | 2 +- priv/repo/seeds.exs | 22 ++++++--------------- 4 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 lib/membership/email.ex diff --git a/lib/membership/email.ex b/lib/membership/email.ex new file mode 100644 index 0000000..eacd548 --- /dev/null +++ b/lib/membership/email.ex @@ -0,0 +1,34 @@ +defmodule Mv.Membership.Email do + @constraints [ + match: ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, + trim?: true, + min_length: 5, + max_length: 254 + ] + + use Ash.Type.NewType, + subtype_of: :string, + constraints: @constraints + + @impl true + def cast_input(value, _) when is_binary(value) do + value = if @constraints[:trim?], do: String.trim(value), else: value + + cond do + @constraints[:min_length] && String.length(value) < @constraints[:min_length] -> + :error + + @constraints[:max_length] && String.length(value) > @constraints[:max_length] -> + :error + + @constraints[:match] && !Regex.match?(@constraints[:match], value) -> + :error + + true -> + {:ok, value} + end + end + + @impl true + def cast_input(_, _), do: :error +end diff --git a/lib/membership/property.ex b/lib/membership/property.ex index 4e96731..0bd5eab 100644 --- a/lib/membership/property.ex +++ b/lib/membership/property.ex @@ -23,7 +23,8 @@ defmodule Mv.Membership.Property do boolean: [type: :boolean], date: [type: :date], integer: [type: :integer], - string: [type: :string] + string: [type: :string], + email: [type: Mv.Membership.Email] ] ] end diff --git a/lib/membership/property_type.ex b/lib/membership/property_type.ex index 9bf16c9..8e42fa6 100644 --- a/lib/membership/property_type.ex +++ b/lib/membership/property_type.ex @@ -19,7 +19,7 @@ defmodule Mv.Membership.PropertyType do attribute :name, :string, allow_nil?: false, public?: true attribute :value_type, :atom, - constraints: [one_of: [:string, :integer, :boolean, :date]], + constraints: [one_of: [:string, :integer, :boolean, :date, :email]], allow_nil?: false, description: "Definies the datatype `Property.value` is interpreted as" diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 27daf36..39327d0 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -2,13 +2,6 @@ # # mix run priv/repo/seeds.exs # -# Inside the script, you can read and write to any of your -# repositories directly: -# -# Mv.Repo.insert!(%Mv.SomeSchema{}) -# -# We recommend using the bang functions (`insert!`, `update!` -# and so on) as they will fail if something goes wrong. alias Mv.Membership @@ -43,18 +36,15 @@ for attrs <- [ }, %{ name: "Email", - value_type: :string, + value_type: :email, description: "Email-Adresse des Mitglieds", immutable: true, required: true } ] do - # upsert?: true sorgt dafür, dass bei bestehendem Namen kein Fehler, - # sondern ein Update (hier effektiv No-Op) ausgeführt wird - {:ok, _} = - Membership.create_property_type( - attrs, - upsert?: true, - upsert_identity: :unique_name - ) + Membership.create_property_type!( + attrs, + upsert?: true, + upsert_identity: :unique_name + ) end From d641711ecfcae4ad919ae420c1a0d5239d4964b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Thu, 29 May 2025 10:58:10 +0200 Subject: [PATCH 07/12] fix(ci): Explicitly pass github token to renovate job --- .drone.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index 7012ac5..819f73f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -86,8 +86,8 @@ steps: RENOVATE_CONFIG_FILE: "renovate_backend_config.js" RENOVATE_TOKEN: from_secret: RENOVATE_TOKEN - #GITHUB_COM_TOKEN: - # from_secret: GITHUB_COM_TOKEN + GITHUB_COM_TOKEN: + from_secret: GITHUB_COM_TOKEN commands: # https://github.com/renovatebot/renovate/discussions/15049 - unset GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL From e3779a73fff41f8dd5aaa3b95b52cfe4c29dc3c0 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 22 May 2025 01:57:01 +0200 Subject: [PATCH 08/12] chore: add regen_migrations script and seed-database to Justfile --- Justfile | 7 +++++-- regen_migrations.sh | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100755 regen_migrations.sh diff --git a/Justfile b/Justfile index b3541fd..e6c81a5 100644 --- a/Justfile +++ b/Justfile @@ -1,4 +1,4 @@ -run: install-dependencies start-database migrate-database +run: install-dependencies start-database migrate-database seed-database mix phx.server install-dependencies: @@ -10,6 +10,9 @@ migrate-database: reset-database: mix ash.reset +seed-database: + mix run priv/repo/seeds.exs + start-database: docker compose up -d @@ -36,4 +39,4 @@ build-docker-container: # This is meant for debugging the container build process only. run-docker-container: build-docker-container - docker run -e "SECRET_KEY_BASE=ahK8BeiDaibaige1ahkooS0chie9lo7the7uuzar0eeBeeCh2iereteshee2Oosu" -e='DATABASE_URL=postgres://postgres@localhost:5432/mv_dev' -e='PORT=4040' -e='PHX_HOST=localhost' --network=host mitgliederverwaltung \ No newline at end of file + docker run -e "SECRET_KEY_BASE=ahK8BeiDaibaige1ahkooS0chie9lo7the7uuzar0eeBeeCh2iereteshee2Oosu" -e='DATABASE_URL=postgres://postgres@localhost:5432/mv_dev' -e='PORT=4040' -e='PHX_HOST=localhost' --network=host mitgliederverwaltung diff --git a/regen_migrations.sh b/regen_migrations.sh new file mode 100755 index 0000000..34cd4b0 --- /dev/null +++ b/regen_migrations.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Get count of untracked migrations +N_MIGRATIONS=$(git ls-files --others priv/repo/migrations | wc -l) + +# Rollback untracked migrations +mix ash_postgres.rollback -n $N_MIGRATIONS + +# Delete untracked migrations and snapshots +git ls-files --others priv/repo/migrations | xargs rm +git ls-files --others priv/resource_snapshots | xargs rm + +# Regenerate migrations +mix ash.codegen --name $1 + +# Run migrations if flag +if echo $* | grep -e "-m" -q; then + mix ash.migrate +fi From b849cfa3df46e13e4ab6520e961c5cd34b2930c9 Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 28 May 2025 18:39:08 +0200 Subject: [PATCH 09/12] property value as Union type --- Justfile | 17 +++++++++++++ lib/membership/property.ex | 16 ++++++++++-- lib/membership/property_type.ex | 5 ++-- lib/mv_web/member_live/form_component.ex | 25 +++++++++++++++++-- ...s => 20250528163901_initial_migration.exs} | 4 +-- priv/repo/seeds.exs | 25 +++++++++++++++++-- ...0250514151922.json => 20250528163901.json} | 2 +- ...0250514151922.json => 20250528163901.json} | 4 +-- ...0250514151922.json => 20250528163901.json} | 4 +-- regen_migrations.sh | 19 -------------- 10 files changed, 87 insertions(+), 34 deletions(-) rename priv/repo/migrations/{20250514151922_initial_migration.exs => 20250528163901_initial_migration.exs} (96%) rename priv/resource_snapshots/repo/members/{20250514151922.json => 20250528163901.json} (87%) rename priv/resource_snapshots/repo/properties/{20250514151922.json => 20250528163901.json} (95%) rename priv/resource_snapshots/repo/property_types/{20250514151922.json => 20250528163901.json} (94%) delete mode 100755 regen_migrations.sh diff --git a/Justfile b/Justfile index e6c81a5..e05e61a 100644 --- a/Justfile +++ b/Justfile @@ -40,3 +40,20 @@ build-docker-container: # This is meant for debugging the container build process only. run-docker-container: build-docker-container docker run -e "SECRET_KEY_BASE=ahK8BeiDaibaige1ahkooS0chie9lo7the7uuzar0eeBeeCh2iereteshee2Oosu" -e='DATABASE_URL=postgres://postgres@localhost:5432/mv_dev' -e='PORT=4040' -e='PHX_HOST=localhost' --network=host mitgliederverwaltung + +regen-migrations migration_name: + #!/bin/bash + set -euo pipefail + # Get count of untracked migrations + N_MIGRATIONS=$(git ls-files --others priv/repo/migrations | wc -l) + # Rollback untracked migrations + mix ash_postgres.rollback -n $N_MIGRATIONS + # Delete untracked migrations and snapshots + git ls-files --others priv/repo/migrations | xargs rm + git ls-files --others priv/resource_snapshots | xargs rm + # Regenerate migrations + mix ash.codegen --name {{migration_name}} + # Run migrations if flag + if echo $* | grep -e "-m" -q; then + mix ash.migrate + fi diff --git a/lib/membership/property.ex b/lib/membership/property.ex index 433fc63..4e96731 100644 --- a/lib/membership/property.ex +++ b/lib/membership/property.ex @@ -16,8 +16,16 @@ defmodule Mv.Membership.Property do attributes do uuid_primary_key :id - attribute :value, :string, - description: "Speichert den Wert, Typ-Interpretation per property_type.typ" + attribute :value, :union, + constraints: [ + storage: :type_and_value, + types: [ + boolean: [type: :boolean], + date: [type: :date], + integer: [type: :integer], + string: [type: :string] + ] + ] end relationships do @@ -25,4 +33,8 @@ defmodule Mv.Membership.Property do belongs_to :property_type, Mv.Membership.PropertyType end + + calculations do + calculate :value_to_string, :string, expr(value[:value] <> "") + end end diff --git a/lib/membership/property_type.ex b/lib/membership/property_type.ex index 560f679..9bf16c9 100644 --- a/lib/membership/property_type.ex +++ b/lib/membership/property_type.ex @@ -10,7 +10,7 @@ defmodule Mv.Membership.PropertyType do actions do defaults [:create, :read, :update, :destroy] - default_accept [:name, :type, :description, :immutable, :required] + default_accept [:name, :value_type, :description, :immutable, :required] end attributes do @@ -18,7 +18,8 @@ defmodule Mv.Membership.PropertyType do attribute :name, :string, allow_nil?: false, public?: true - attribute :type, :string, + attribute :value_type, :atom, + constraints: [one_of: [:string, :integer, :boolean, :date]], allow_nil?: false, description: "Definies the datatype `Property.value` is interpreted as" diff --git a/lib/mv_web/member_live/form_component.ex b/lib/mv_web/member_live/form_component.ex index b91d3c0..9db46b0 100644 --- a/lib/mv_web/member_live/form_component.ex +++ b/lib/mv_web/member_live/form_component.ex @@ -9,7 +9,11 @@ defmodule MvWeb.MemberLive.FormComponent do Enum.map(property_types, fn pt -> %{ "property_type_id" => pt.id, - "value" => nil + "value" => %{ + "type" => pt.value_type, + "value" => nil, + "_union_type" => Atom.to_string(pt.value_type) + } } end) @@ -34,7 +38,9 @@ defmodule MvWeb.MemberLive.FormComponent do > <.inputs_for :let={f_property} field={@form[:properties]}> <% type = Enum.find(@property_types, &(&1.id == f_property[:property_type_id].value)) %> - <.input field={f_property[:value]} label={type && type.name} /> + <.inputs_for :let={value_form} field={f_property[:value]}> + <.input field={value_form[:value]} label={type && type.name} /> + + Enum.map(member.properties, fn prop -> + %{ + "property_type_id" => prop.property_type_id, + "value" => %{ + "_union_type" => Atom.to_string(prop.value.type), + "type" => prop.value.type, + "value" => prop.value.value + } + } + end) + } + form = AshPhoenix.Form.for_update( member, :update_member, api: Mv.Membership, as: "member", + params: params, forms: [auto?: true] ) diff --git a/priv/repo/migrations/20250514151922_initial_migration.exs b/priv/repo/migrations/20250528163901_initial_migration.exs similarity index 96% rename from priv/repo/migrations/20250514151922_initial_migration.exs rename to priv/repo/migrations/20250528163901_initial_migration.exs index 64915c2..bf36e43 100644 --- a/priv/repo/migrations/20250514151922_initial_migration.exs +++ b/priv/repo/migrations/20250528163901_initial_migration.exs @@ -11,7 +11,7 @@ defmodule Mv.Repo.Migrations.InitialMigration do create table(:property_types, primary_key: false) do add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true add :name, :text, null: false - add :type, :text, null: false + add :value_type, :text, null: false add :description, :text add :immutable, :boolean, null: false, default: false add :required, :boolean, null: false, default: false @@ -21,7 +21,7 @@ defmodule Mv.Repo.Migrations.InitialMigration do create table(:properties, primary_key: false) do add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true - add :value, :text + add :value, :map add :member_id, :uuid add :property_type_id, :uuid end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 8fca144..27daf36 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -15,14 +15,35 @@ alias Mv.Membership for attrs <- [ %{ name: "Vorname", - type: "string", + value_type: :string, description: "Vorname des Mitglieds", immutable: true, required: true }, + %{ + name: "Nachname", + value_type: :string, + description: "Nachname des Mitglieds", + immutable: true, + required: true + }, + %{ + name: "Geburtsdatum", + value_type: :date, + description: "Geburtsdatum des Mitglieds", + immutable: true, + required: true + }, + %{ + name: "Bezahlt", + value_type: :boolean, + description: "Status des Mitgliedsbeitrages des Mitglieds", + immutable: true, + required: true + }, %{ name: "Email", - type: "string", + value_type: :string, description: "Email-Adresse des Mitglieds", immutable: true, required: true diff --git a/priv/resource_snapshots/repo/members/20250514151922.json b/priv/resource_snapshots/repo/members/20250528163901.json similarity index 87% rename from priv/resource_snapshots/repo/members/20250514151922.json rename to priv/resource_snapshots/repo/members/20250528163901.json index df811d8..2d08b17 100644 --- a/priv/resource_snapshots/repo/members/20250514151922.json +++ b/priv/resource_snapshots/repo/members/20250528163901.json @@ -16,7 +16,7 @@ "custom_indexes": [], "custom_statements": [], "has_create_action": true, - "hash": "A0402269CB456075B81CA4CB3A2135A2C88D8B7FD51CD7A23084AA5264FEE344", + "hash": "35D45214D6D344B0AF6CFCB69B8682FCB3D382D85883D3D3AAC1AEE7F54FD89A", "identities": [], "multitenancy": { "attribute": null, diff --git a/priv/resource_snapshots/repo/properties/20250514151922.json b/priv/resource_snapshots/repo/properties/20250528163901.json similarity index 95% rename from priv/resource_snapshots/repo/properties/20250514151922.json rename to priv/resource_snapshots/repo/properties/20250528163901.json index 0707bcf..daf3222 100644 --- a/priv/resource_snapshots/repo/properties/20250514151922.json +++ b/priv/resource_snapshots/repo/properties/20250528163901.json @@ -18,7 +18,7 @@ "references": null, "size": null, "source": "value", - "type": "text" + "type": "map" }, { "allow_nil?": true, @@ -84,7 +84,7 @@ "custom_indexes": [], "custom_statements": [], "has_create_action": true, - "hash": "F2A42A3427E0428637F465E4F357A3BE21B33231F94CF77B4843084128F6BDA5", + "hash": "8CF241CB9E8239511914EDEC96186BB7879529372BD8A4162431CCE9961F4F1B", "identities": [], "multitenancy": { "attribute": null, diff --git a/priv/resource_snapshots/repo/property_types/20250514151922.json b/priv/resource_snapshots/repo/property_types/20250528163901.json similarity index 94% rename from priv/resource_snapshots/repo/property_types/20250514151922.json rename to priv/resource_snapshots/repo/property_types/20250528163901.json index 5da5fa8..55f066a 100644 --- a/priv/resource_snapshots/repo/property_types/20250514151922.json +++ b/priv/resource_snapshots/repo/property_types/20250528163901.json @@ -27,7 +27,7 @@ "primary_key?": false, "references": null, "size": null, - "source": "type", + "source": "value_type", "type": "text" }, { @@ -66,7 +66,7 @@ "custom_indexes": [], "custom_statements": [], "has_create_action": true, - "hash": "47210108DE1E7B2A20A67205E875B3440526941E61AB95B166976E8CD8AA0955", + "hash": "F98A723AE0D20005FBE4205E46ABEE09A88DFF9334C85BADC1FBEEF100F3E25B", "identities": [ { "all_tenants?": false, diff --git a/regen_migrations.sh b/regen_migrations.sh deleted file mode 100755 index 34cd4b0..0000000 --- a/regen_migrations.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Get count of untracked migrations -N_MIGRATIONS=$(git ls-files --others priv/repo/migrations | wc -l) - -# Rollback untracked migrations -mix ash_postgres.rollback -n $N_MIGRATIONS - -# Delete untracked migrations and snapshots -git ls-files --others priv/repo/migrations | xargs rm -git ls-files --others priv/resource_snapshots | xargs rm - -# Regenerate migrations -mix ash.codegen --name $1 - -# Run migrations if flag -if echo $* | grep -e "-m" -q; then - mix ash.migrate -fi From 723d9c7205bd90f1064e15e6fbe2fa49540ea4a8 Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 28 May 2025 19:02:42 +0200 Subject: [PATCH 10/12] choose input filed type by value_type --- lib/mv_web/member_live/form_component.ex | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/mv_web/member_live/form_component.ex b/lib/mv_web/member_live/form_component.ex index 9db46b0..101cf6c 100644 --- a/lib/mv_web/member_live/form_component.ex +++ b/lib/mv_web/member_live/form_component.ex @@ -39,7 +39,13 @@ defmodule MvWeb.MemberLive.FormComponent do <.inputs_for :let={f_property} field={@form[:properties]}> <% type = Enum.find(@property_types, &(&1.id == f_property[:property_type_id].value)) %> <.inputs_for :let={value_form} field={f_property[:value]}> - <.input field={value_form[:value]} label={type && type.name} /> + <% input_type = + cond do + type && type.value_type == :boolean -> "checkbox" + type && type.value_type == :date -> :date + true -> :text + end %> + <.input field={value_form[:value]} label={type && type.name} type={input_type} /> Date: Wed, 28 May 2025 19:51:34 +0200 Subject: [PATCH 11/12] chore(Justfile): allow regenerating migrations by commit hash --- Justfile | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/Justfile b/Justfile index e05e61a..13e8a01 100644 --- a/Justfile +++ b/Justfile @@ -41,19 +41,33 @@ build-docker-container: run-docker-container: build-docker-container docker run -e "SECRET_KEY_BASE=ahK8BeiDaibaige1ahkooS0chie9lo7the7uuzar0eeBeeCh2iereteshee2Oosu" -e='DATABASE_URL=postgres://postgres@localhost:5432/mv_dev' -e='PORT=4040' -e='PHX_HOST=localhost' --network=host mitgliederverwaltung -regen-migrations migration_name: - #!/bin/bash +# Usage: +# just regen-migrations migration_name [commit_hash] +# If commit_hash is given, rollback & delete the migrations from that commit. +# Otherwise, rollback & delete all untracked migrations. +regen-migrations migration_name commit_hash='': + #!/usr/bin/env bash set -euo pipefail - # Get count of untracked migrations - N_MIGRATIONS=$(git ls-files --others priv/repo/migrations | wc -l) - # Rollback untracked migrations - mix ash_postgres.rollback -n $N_MIGRATIONS - # Delete untracked migrations and snapshots - git ls-files --others priv/repo/migrations | xargs rm - git ls-files --others priv/resource_snapshots | xargs rm - # Regenerate migrations - mix ash.codegen --name {{migration_name}} - # Run migrations if flag - if echo $* | grep -e "-m" -q; then - mix ash.migrate + # Pick migrations either from the given commit or untracked files + if [ -n "{{commit_hash}}" ]; then + echo "→ Rolling back migrations from commit {{commit_hash}}" + MIG_FILES=$(git show --name-only --pretty=format: "{{commit_hash}}" \ + | grep -E "^priv/repo/migrations/|^priv/resource_snapshots") + else + echo "→ Rolling back all untracked migrations" + MIG_FILES=$(git ls-files --others priv/repo/migrations) fi + + # Roll back in Ash + COUNT=$(echo "$MIG_FILES" | wc -l) + mix ash_postgres.rollback -n "$COUNT" + + # Remove the migration files + echo removing $MIG_FILES + echo "$MIG_FILES" | xargs rm -f + + # Also clean up any untracked resource snapshots + git ls-files --others priv/resource_snapshots | xargs rm -f + + # Generate a fresh migration + mix ash.codegen --name "{{migration_name}}" From 859f5f4497391fc96a0a06c541bf4c844221b5f5 Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 28 May 2025 22:04:54 +0200 Subject: [PATCH 12/12] feat: add custom email type for validation --- lib/membership/email.ex | 34 +++++++++++++++++++++++++++++++++ lib/membership/property.ex | 3 ++- lib/membership/property_type.ex | 2 +- priv/repo/seeds.exs | 22 ++++++--------------- 4 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 lib/membership/email.ex diff --git a/lib/membership/email.ex b/lib/membership/email.ex new file mode 100644 index 0000000..eacd548 --- /dev/null +++ b/lib/membership/email.ex @@ -0,0 +1,34 @@ +defmodule Mv.Membership.Email do + @constraints [ + match: ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, + trim?: true, + min_length: 5, + max_length: 254 + ] + + use Ash.Type.NewType, + subtype_of: :string, + constraints: @constraints + + @impl true + def cast_input(value, _) when is_binary(value) do + value = if @constraints[:trim?], do: String.trim(value), else: value + + cond do + @constraints[:min_length] && String.length(value) < @constraints[:min_length] -> + :error + + @constraints[:max_length] && String.length(value) > @constraints[:max_length] -> + :error + + @constraints[:match] && !Regex.match?(@constraints[:match], value) -> + :error + + true -> + {:ok, value} + end + end + + @impl true + def cast_input(_, _), do: :error +end diff --git a/lib/membership/property.ex b/lib/membership/property.ex index 4e96731..0bd5eab 100644 --- a/lib/membership/property.ex +++ b/lib/membership/property.ex @@ -23,7 +23,8 @@ defmodule Mv.Membership.Property do boolean: [type: :boolean], date: [type: :date], integer: [type: :integer], - string: [type: :string] + string: [type: :string], + email: [type: Mv.Membership.Email] ] ] end diff --git a/lib/membership/property_type.ex b/lib/membership/property_type.ex index 9bf16c9..8e42fa6 100644 --- a/lib/membership/property_type.ex +++ b/lib/membership/property_type.ex @@ -19,7 +19,7 @@ defmodule Mv.Membership.PropertyType do attribute :name, :string, allow_nil?: false, public?: true attribute :value_type, :atom, - constraints: [one_of: [:string, :integer, :boolean, :date]], + constraints: [one_of: [:string, :integer, :boolean, :date, :email]], allow_nil?: false, description: "Definies the datatype `Property.value` is interpreted as" diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 27daf36..39327d0 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -2,13 +2,6 @@ # # mix run priv/repo/seeds.exs # -# Inside the script, you can read and write to any of your -# repositories directly: -# -# Mv.Repo.insert!(%Mv.SomeSchema{}) -# -# We recommend using the bang functions (`insert!`, `update!` -# and so on) as they will fail if something goes wrong. alias Mv.Membership @@ -43,18 +36,15 @@ for attrs <- [ }, %{ name: "Email", - value_type: :string, + value_type: :email, description: "Email-Adresse des Mitglieds", immutable: true, required: true } ] do - # upsert?: true sorgt dafür, dass bei bestehendem Namen kein Fehler, - # sondern ein Update (hier effektiv No-Op) ausgeführt wird - {:ok, _} = - Membership.create_property_type( - attrs, - upsert?: true, - upsert_identity: :unique_name - ) + Membership.create_property_type!( + attrs, + upsert?: true, + upsert_identity: :unique_name + ) end