Merge pull request 'property values as maps closes #53' (#56) from property_values into main
Some checks reported errors
continuous-integration/drone/push Build was killed
Some checks reported errors
continuous-integration/drone/push Build was killed
Reviewed-on: #56 Reviewed-by: rafael <rafael@noreply.git.local-it.org>
This commit is contained in:
commit
ff88ab3739
10 changed files with 152 additions and 32 deletions
38
Justfile
38
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,35 @@ 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
|
||||
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
|
||||
|
||||
# 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
|
||||
# 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}}"
|
||||
|
|
|
|||
34
lib/membership/email.ex
Normal file
34
lib/membership/email.ex
Normal file
|
|
@ -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
|
||||
|
|
@ -16,8 +16,17 @@ 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],
|
||||
email: [type: Mv.Membership.Email]
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
relationships do
|
||||
|
|
@ -25,4 +34,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
|
||||
|
|
|
|||
|
|
@ -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, :email]],
|
||||
allow_nil?: false,
|
||||
description: "Definies the datatype `Property.value` is interpreted as"
|
||||
|
||||
|
|
|
|||
|
|
@ -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,15 @@ 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_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} />
|
||||
</.inputs_for>
|
||||
<input
|
||||
type="hidden"
|
||||
name={f_property[:property_type_id].name}
|
||||
|
|
@ -95,12 +107,27 @@ defmodule MvWeb.MemberLive.FormComponent do
|
|||
not Enum.member?(existing_properties, Map.get(i, "property_type_id"))
|
||||
end
|
||||
|
||||
params = %{
|
||||
"properties" =>
|
||||
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]
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -2,38 +2,49 @@
|
|||
#
|
||||
# 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
|
||||
|
||||
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: :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
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "A0402269CB456075B81CA4CB3A2135A2C88D8B7FD51CD7A23084AA5264FEE344",
|
||||
"hash": "35D45214D6D344B0AF6CFCB69B8682FCB3D382D85883D3D3AAC1AEE7F54FD89A",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
Loading…
Add table
Add a link
Reference in a new issue