From dce8fbc232c7b5fee947cb305e67c6f8b3691034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Wed, 14 May 2025 14:20:57 +0200 Subject: [PATCH 01/15] Add Release scripts & Dockerfile --- .dockerignore | 45 +++++++++++++++++++ Dockerfile | 97 ++++++++++++++++++++++++++++++++++++++++ Justfile | 9 +++- config/runtime.exs | 2 +- lib/mv/release.ex | 28 ++++++++++++ rel/overlays/bin/migrate | 5 +++ rel/overlays/bin/server | 5 +++ 7 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 lib/mv/release.ex create mode 100755 rel/overlays/bin/migrate create mode 100755 rel/overlays/bin/server diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..61a7393 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,45 @@ +# This file excludes paths from the Docker build context. +# +# By default, Docker's build context includes all files (and folders) in the +# current directory. Even if a file isn't copied into the container it is still sent to +# the Docker daemon. +# +# There are multiple reasons to exclude files from the build context: +# +# 1. Prevent nested folders from being copied into the container (ex: exclude +# /assets/node_modules when copying /assets) +# 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc) +# 3. Avoid sending files containing sensitive information +# +# More information on using .dockerignore is available here: +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +.dockerignore + +# Ignore git, but keep git HEAD and refs to access current commit hash if needed: +# +# $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat +# d0b8727759e1e0e7aa3d41707d12376e373d5ecc +.git +!.git/HEAD +!.git/refs + +# Common development/test artifacts +/cover/ +/doc/ +/test/ +/tmp/ +.elixir_ls + +# Mix artifacts +/_build/ +/deps/ +*.ez + +# Generated on crash by the VM +erl_crash.dump + +# Static artifacts - These should be fetched and built inside the Docker image +/assets/node_modules/ +/priv/static/assets/ +/priv/static/cache_manifest.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..60e0c6b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,97 @@ +# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian +# instead of Alpine to avoid DNS resolution issues in production. +# +# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu +# https://hub.docker.com/_/ubuntu?tab=tags +# +# This file is based on these images: +# +# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image +# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20250317-slim - for the release image +# - https://pkgs.org/ - resource for finding needed packages +# - Ex: hexpm/elixir:1.18.3-erlang-27.3-debian-bullseye-20250317-slim +# +ARG ELIXIR_VERSION=1.18.3 +ARG OTP_VERSION=27.3 +ARG DEBIAN_VERSION=bullseye-20250317-slim + +ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" +ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" + +FROM ${BUILDER_IMAGE} as builder + +# install build dependencies +RUN apt-get update -y && apt-get install -y build-essential git \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# prepare build dir +WORKDIR /app + +# install hex + rebar +RUN mix local.hex --force && \ + mix local.rebar --force + +# set build ENV +ENV MIX_ENV="prod" + +# install mix dependencies +COPY mix.exs mix.lock ./ +RUN mix deps.get --only $MIX_ENV +RUN mkdir config + +# copy compile-time config files before we compile dependencies +# to ensure any relevant config change will trigger the dependencies +# to be re-compiled. +COPY config/config.exs config/${MIX_ENV}.exs config/ +RUN mix deps.compile + +COPY priv priv + +COPY lib lib + +COPY assets assets + +# compile assets +RUN mix assets.deploy + +# Compile the release +RUN mix compile + +# Changes to config/runtime.exs don't require recompiling the code +COPY config/runtime.exs config/ + +COPY rel rel +RUN mix release + +# start a new build stage so that the final image will only contain +# the compiled release and other runtime necessities +FROM ${RUNNER_IMAGE} + +RUN apt-get update -y && \ + apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# Set the locale +RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen + +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + +WORKDIR "/app" +RUN chown nobody /app + +# set runner ENV +ENV MIX_ENV="prod" + +# Only copy the final release from the build stage +COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/mv ./ + +USER nobody + +# If using an environment that doesn't automatically reap zombie processes, it is +# advised to add an init process such as tini via `apt-get install` +# above and adding an entrypoint. See https://github.com/krallin/tini for details +# ENTRYPOINT ["/tini", "--"] + +CMD ["/app/bin/server"] diff --git a/Justfile b/Justfile index 5f9dbf1..ed412c3 100644 --- a/Justfile +++ b/Justfile @@ -29,4 +29,11 @@ test: mix test format: - mix format \ No newline at end of file + mix format + +build-docker-container: + docker build --tag mitgliederverwaltung . + +# 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 diff --git a/config/runtime.exs b/config/runtime.exs index be23767..e591590 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -48,7 +48,7 @@ if config_env() == :prod do You can generate one by calling: mix phx.gen.secret """ - host = System.get_env("PHX_HOST") || "example.com" + host = System.get_env("PHX_HOST") || raise "Please define the PHX_HOST environment variable." port = String.to_integer(System.get_env("PORT") || "4000") config :mv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") diff --git a/lib/mv/release.ex b/lib/mv/release.ex new file mode 100644 index 0000000..c0c2c8a --- /dev/null +++ b/lib/mv/release.ex @@ -0,0 +1,28 @@ +defmodule Mv.Release do + @moduledoc """ + Used for executing DB release tasks when run in production without Mix + installed. + """ + @app :mv + + def migrate do + load_app() + + for repo <- repos() do + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) + end + end + + def rollback(repo, version) do + load_app() + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) + end + + defp repos do + Application.fetch_env!(@app, :ecto_repos) + end + + defp load_app do + Application.load(@app) + end +end diff --git a/rel/overlays/bin/migrate b/rel/overlays/bin/migrate new file mode 100755 index 0000000..9070709 --- /dev/null +++ b/rel/overlays/bin/migrate @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +exec ./mv eval Mv.Release.migrate diff --git a/rel/overlays/bin/server b/rel/overlays/bin/server new file mode 100755 index 0000000..e239b21 --- /dev/null +++ b/rel/overlays/bin/server @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +PHX_SERVER=true exec ./mv start From cf18d304eebc6f3e25a9f5e7bf0de8db9ae88a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Thu, 22 May 2025 12:02:55 +0200 Subject: [PATCH 02/15] chore: Ignore elixir dependency in renovate It's a bit complicated to support the `-otp` postfix right now, so to fix this right now, we'll just disable automatic updates for elixir. --- renovate.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/renovate.json b/renovate.json index 316f0a8..f5f875c 100644 --- a/renovate.json +++ b/renovate.json @@ -10,6 +10,11 @@ "groupName": "asdf tool versions", "description": "Keep in mind that Renovate currently does not support updating PostgreSQL via asdf.", "matchFileNames": [".tool-versions"] + }, + { + "matchFileNames": [".tool-versions"], + "matchPackageNames": ["elixir"], + "enabled": false } ] } From 41634711fe1f09ece85213a0ccf8ecddeb4b62cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Thu, 22 May 2025 12:14:00 +0200 Subject: [PATCH 03/15] chore: Remove renovate comment about postgresql in asdf Since postgres is now handled via docker-compose instead of asdf, the restriction mentioned in the comment does not apply anymore. --- renovate.json | 1 - 1 file changed, 1 deletion(-) diff --git a/renovate.json b/renovate.json index f5f875c..ed294b5 100644 --- a/renovate.json +++ b/renovate.json @@ -8,7 +8,6 @@ }, { "groupName": "asdf tool versions", - "description": "Keep in mind that Renovate currently does not support updating PostgreSQL via asdf.", "matchFileNames": [".tool-versions"] }, { From ddee222f86b7a64b4034496f07b9e4257aa74f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Thu, 22 May 2025 12:18:54 +0200 Subject: [PATCH 04/15] chore: Group renovate postgres updates --- renovate.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/renovate.json b/renovate.json index ed294b5..3b89620 100644 --- a/renovate.json +++ b/renovate.json @@ -10,6 +10,11 @@ "groupName": "asdf tool versions", "matchFileNames": [".tool-versions"] }, + { + "groupName": "postgres", + "description": "Group updates to postgres across drone ci, docker-compose.yml and other files", + "matchDepNames": ["postgres"] + }, { "matchFileNames": [".tool-versions"], "matchPackageNames": ["elixir"], From 32fafad196befa5553a02499a92db16c6b07c799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Thu, 22 May 2025 12:34:42 +0200 Subject: [PATCH 05/15] chore: Also run renovate on push to main branch --- .drone.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.drone.yml b/.drone.yml index b9fa96e..bb82576 100644 --- a/.drone.yml +++ b/.drone.yml @@ -72,6 +72,9 @@ trigger: event: - cron - custom + - push + branch: + - main environment: LOG_LEVEL: debug From a334ab6bc479e3579519d9d99382cdc79e78cd77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Thu, 22 May 2025 12:42:57 +0200 Subject: [PATCH 06/15] chore(renovate): Use glob patterns to match postgres packages --- renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 3b89620..fadc4d2 100644 --- a/renovate.json +++ b/renovate.json @@ -13,7 +13,7 @@ { "groupName": "postgres", "description": "Group updates to postgres across drone ci, docker-compose.yml and other files", - "matchDepNames": ["postgres"] + "matchDepNames": ["**postgres**"] }, { "matchFileNames": [".tool-versions"], From 9e7d8b227755302e68471ee20ab9170580970865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Thu, 22 May 2025 12:46:23 +0200 Subject: [PATCH 07/15] chore: Remove variables from dockerfile base images to enable renovate updates --- Dockerfile | 8 ++------ renovate.json | 6 +++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 60e0c6b..5503e2e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,12 +11,8 @@ # - https://pkgs.org/ - resource for finding needed packages # - Ex: hexpm/elixir:1.18.3-erlang-27.3-debian-bullseye-20250317-slim # -ARG ELIXIR_VERSION=1.18.3 -ARG OTP_VERSION=27.3 -ARG DEBIAN_VERSION=bullseye-20250317-slim - -ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" -ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" +ARG BUILDER_IMAGE="hexpm/elixir:1.18.3-erlang-27.3-debian-bullseye-20250317-slim" +ARG RUNNER_IMAGE="debian:bullseye-20250317-slim" FROM ${BUILDER_IMAGE} as builder diff --git a/renovate.json b/renovate.json index fadc4d2..6f76f2a 100644 --- a/renovate.json +++ b/renovate.json @@ -13,11 +13,11 @@ { "groupName": "postgres", "description": "Group updates to postgres across drone ci, docker-compose.yml and other files", - "matchDepNames": ["**postgres**"] + "matchPackageNames": ["**postgres**"] }, { - "matchFileNames": [".tool-versions"], - "matchPackageNames": ["elixir"], + "matchFileNames": [".tool-versions", "Dockerfile"], + "matchCurrentValue": "**elixir-otp-**", "enabled": false } ] From 60f20ceacff5969433daab5ddf575e35fa0842b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Thu, 22 May 2025 12:58:34 +0200 Subject: [PATCH 08/15] fix: Elixir version matching in renovate config --- renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 6f76f2a..111dc1b 100644 --- a/renovate.json +++ b/renovate.json @@ -17,7 +17,7 @@ }, { "matchFileNames": [".tool-versions", "Dockerfile"], - "matchCurrentValue": "**elixir-otp-**", + "matchCurrentValue": "**-otp-**", "enabled": false } ] From aeb9cb8e2989fd675739d2149931e9b5c71746ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Thu, 22 May 2025 13:06:08 +0200 Subject: [PATCH 09/15] fix(renovate): Exclude elixir dependencies from postgres update --- renovate.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 111dc1b..7e12828 100644 --- a/renovate.json +++ b/renovate.json @@ -13,7 +13,8 @@ { "groupName": "postgres", "description": "Group updates to postgres across drone ci, docker-compose.yml and other files", - "matchPackageNames": ["**postgres**"] + "matchPackageNames": ["postgres", "docker.io/library/postgres"], + "matchDatasources": ["docker"] }, { "matchFileNames": [".tool-versions", "Dockerfile"], From 0d33f1baf72621456a38608456e49c5726fa9d9a Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 22 May 2025 10:24:42 +0000 Subject: [PATCH 10/15] chore(deps): update renovate/renovate docker tag to v40 --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index bb82576..7012ac5 100644 --- a/.drone.yml +++ b/.drone.yml @@ -81,7 +81,7 @@ environment: steps: - name: renovate - image: renovate/renovate:39.264 + image: renovate/renovate:40.22 environment: RENOVATE_CONFIG_FILE: "renovate_backend_config.js" RENOVATE_TOKEN: From feb747bd2130ba77952505a1844827c7dbbf84d5 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 22 May 2025 01:57:01 +0200 Subject: [PATCH 11/15] 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 12/15] 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 13/15] 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 14/15] 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 780efc3e915bbc62054fe0f75991a9faec95e1fa Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 28 May 2025 22:04:54 +0200 Subject: [PATCH 15/15] 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..4acf2b9 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