refactor: Rename Property/PropertyType to CustomFieldValue/CustomField
All checks were successful
continuous-integration/drone/push Build is passing

Complete refactoring of resources, database tables, code references, tests, and documentation for improved naming consistency.
This commit is contained in:
Moritz 2025-11-13 17:58:12 +01:00
parent 47f18e9ef3
commit 8400e727a7
Signed by: moritz
GPG key ID: 1020A035E5DD0824
31 changed files with 1002 additions and 647 deletions

View file

@ -52,21 +52,21 @@ This document provides a comprehensive overview of the Mila Membership Managemen
- Bidirectional email sync with users
- Flexible address and contact data
#### `properties`
#### `custom_field_values`
- **Purpose:** Dynamic custom member attributes
- **Rows (Estimated):** Variable (N per member)
- **Key Features:**
- Union type value storage (JSONB)
- Multiple data types supported
- One property per type per member
- One custom field value per custom field per member
#### `property_types`
- **Purpose:** Schema definitions for custom properties
#### `custom_fields`
- **Purpose:** Schema definitions for custom_field_values
- **Rows (Estimated):** Low (admin-defined)
- **Key Features:**
- Type definitions
- Immutable and required flags
- Centralized property management
- Centralized custom field management
## Key Relationships
@ -77,7 +77,7 @@ User (0..1) ←→ (0..1) Member
Member (1) → (N) Properties
PropertyType (1)
CustomField (1)
```
### Relationship Details
@ -90,11 +90,11 @@ Member (1) → (N) Properties
- `ON DELETE SET NULL` on user side (User preserved when Member deleted)
2. **Member → Properties (1:N)**
- One member, many properties
- `ON DELETE CASCADE` - properties deleted with member
- Composite unique constraint (member_id, property_type_id)
- One member, many custom_field_values
- `ON DELETE CASCADE` - custom_field_values deleted with member
- Composite unique constraint (member_id, custom_field_id)
3. **Property → PropertyType (N:1)**
3. **CustomFieldValue → CustomField (N:1)**
- Properties reference type definition
- `ON DELETE RESTRICT` - cannot delete type if in use
- Type defines data structure
@ -121,8 +121,8 @@ Member (1) → (N) Properties
- Phone: `+?[0-9\- ]{6,20}`
- Postal code: 5 digits
### Property System
- Maximum one property per type per member
### CustomFieldValue System
- Maximum one custom field value per custom field per member
- Value stored as union type in JSONB
- Supported types: string, integer, boolean, date, email
- Types can be marked as immutable or required
@ -144,10 +144,10 @@ Member (1) → (N) Properties
- `join_date` (B-tree) - Date filtering
- `paid` (partial B-tree) - Payment status queries
**properties:**
- `member_id` - Member property lookups
- `property_type_id` - Type-based queries
- Composite `(member_id, property_type_id)` - Uniqueness
**custom_field_values:**
- `member_id` - Member custom field value lookups
- `custom_field_id` - Type-based queries
- Composite `(member_id, custom_field_id)` - Uniqueness
**tokens:**
- `subject` - User token lookups
@ -297,8 +297,8 @@ priv/repo/migrations/
| Relationship | On Delete | Rationale |
|--------------|-----------|-----------|
| `users.member_id → members.id` | SET NULL | Preserve user account when member deleted |
| `properties.member_id → members.id` | CASCADE | Delete properties with member |
| `properties.property_type_id → property_types.id` | RESTRICT | Prevent deletion of types in use |
| `custom_field_values.member_id → members.id` | CASCADE | Delete custom_field_values with member |
| `custom_field_values.custom_field_id → custom_fields.id` | RESTRICT | Prevent deletion of types in use |
### Validation Layers
@ -327,15 +327,15 @@ priv/repo/migrations/
- Member search (uses GIN index on search_vector)
- Member list with filters (uses indexes on join_date, paid)
- User authentication (uses unique index on email/oidc_id)
- Property lookups by member (uses index on member_id)
- CustomFieldValue lookups by member (uses index on member_id)
**Medium Frequency:**
- Member CRUD operations
- Property updates
- CustomFieldValue updates
- Token validation
**Low Frequency:**
- PropertyType management
- CustomField management
- User-Member linking
- Bulk operations
@ -396,10 +396,10 @@ Install "DBML Language" extension to view/edit DBML files with:
### Critical Tables (Priority 1)
- `members` - Core business data
- `users` - Authentication data
- `property_types` - Schema definitions
- `custom_fields` - Schema definitions
### Important Tables (Priority 2)
- `properties` - Member custom data
- `custom_field_values` - Member custom data
- `tokens` - Can be regenerated but good to backup
### Backup Strategy

View file

@ -18,7 +18,7 @@ Project mila_membership_management {
## Key Features:
- User authentication (OIDC + Password with secure account linking)
- Member management with flexible custom properties
- Member management with flexible custom fields
- Bidirectional email synchronization between users and members
- Full-text search capabilities (tsvector)
- Fuzzy search with trigram matching (pg_trgm)
@ -26,7 +26,7 @@ Project mila_membership_management {
## Domains:
- **Accounts**: User authentication and session management
- **Membership**: Club member data and custom properties
- **Membership**: Club member data and custom fields
## Required PostgreSQL Extensions:
- uuid-ossp (UUID generation)
@ -178,7 +178,7 @@ Table members {
**Relationships:**
- Optional 1:1 with users (0..1 ↔ 0..1) - authentication account
- 1:N with properties (custom dynamic fields)
- 1:N with custom_field_values (custom dynamic fields)
**Validation Rules:**
- first_name, last_name: min 1 character
@ -191,20 +191,20 @@ Table members {
'''
}
Table properties {
Table custom_field_values {
id uuid [pk, not null, default: `gen_random_uuid()`, note: 'Primary identifier']
value jsonb [null, note: 'Union type value storage (format: {type: "string", value: "example"})']
member_id uuid [not null, note: 'Link to member']
property_type_id uuid [not null, note: 'Link to property type definition']
custom_field_id uuid [not null, note: 'Link to custom field definition']
indexes {
(member_id, property_type_id) [unique, name: 'properties_unique_property_per_member_index', note: 'One property per type per member']
member_id [name: 'properties_member_id_idx']
property_type_id [name: 'properties_property_type_id_idx']
(member_id, custom_field_id) [unique, name: 'custom_field_values_unique_custom_field_per_member_index', note: 'One custom field value per custom field per member']
member_id [name: 'custom_field_values_member_id_idx']
custom_field_id [name: 'custom_field_values_custom_field_id_idx']
}
Note: '''
**Dynamic Custom Member Properties**
**Dynamic Custom Member Field Values**
Provides flexible, extensible attributes for members beyond the fixed schema.
@ -221,9 +221,9 @@ Table properties {
- `email`: Validated email addresses
**Constraints:**
- Each member can have only ONE property per property_type
- Properties are deleted when member is deleted (CASCADE)
- Property type cannot be deleted if properties exist (RESTRICT)
- Each member can have only ONE custom field value per custom field
- Custom field values are deleted when member is deleted (CASCADE)
- Custom field cannot be deleted if custom field values exist (RESTRICT)
**Use Cases:**
- Custom membership numbers
@ -233,34 +233,34 @@ Table properties {
'''
}
Table property_types {
Table custom_fields {
id uuid [pk, not null, default: `gen_random_uuid()`, note: 'Primary identifier']
name text [not null, unique, note: 'Property name/identifier (e.g., "membership_number")']
name text [not null, unique, note: 'CustomFieldValue name/identifier (e.g., "membership_number")']
value_type text [not null, note: 'Data type: string | integer | boolean | date | email']
description text [null, note: 'Human-readable description']
immutable boolean [not null, default: false, note: 'If true, value cannot be changed after creation']
required boolean [not null, default: false, note: 'If true, all members must have this property']
required boolean [not null, default: false, note: 'If true, all members must have this custom field']
indexes {
name [unique, name: 'property_types_unique_name_index']
name [unique, name: 'custom_fields_unique_name_index']
}
Note: '''
**Property Type Definitions**
**CustomFieldValue Type Definitions**
Defines the schema and behavior for custom member properties.
Defines the schema and behavior for custom member custom_field_values.
**Attributes:**
- `name`: Unique identifier for the property type
- `name`: Unique identifier for the custom field
- `value_type`: Enforces data type consistency
- `description`: Documentation for users/admins
- `immutable`: Prevents changes after initial creation (e.g., membership numbers)
- `required`: Enforces that all members must have this property
- `required`: Enforces that all members must have this custom field
**Constraints:**
- `value_type` must be one of: string, integer, boolean, date, email
- `name` must be unique across all property types
- Cannot be deleted if properties reference it (ON DELETE RESTRICT)
- `name` must be unique across all custom fields
- Cannot be deleted if custom_field_values reference it (ON DELETE RESTRICT)
**Examples:**
- Membership Number (string, immutable, required)
@ -283,25 +283,25 @@ Table property_types {
Ref: users.member_id - members.id [delete: set null]
// Member → Properties (1:N)
// - One member can have multiple properties
// - Each property belongs to exactly one member
// - One member can have multiple custom_field_values
// - Each custom field value belongs to exactly one member
// - ON DELETE CASCADE: Properties deleted when member deleted
// - UNIQUE constraint: One property per type per member
Ref: properties.member_id > members.id [delete: cascade]
// - UNIQUE constraint: One custom field value per custom field per member
Ref: custom_field_values.member_id > members.id [delete: cascade]
// Property → PropertyType (N:1)
// - Many properties can reference one property type
// - Property type defines the schema/behavior
// - ON DELETE RESTRICT: Cannot delete type if properties exist
Ref: properties.property_type_id > property_types.id [delete: restrict]
// CustomFieldValue → CustomField (N:1)
// - Many custom_field_values can reference one custom field
// - CustomFieldValue type defines the schema/behavior
// - ON DELETE RESTRICT: Cannot delete type if custom_field_values exist
Ref: custom_field_values.custom_field_id > custom_fields.id [delete: restrict]
// ============================================
// ENUMS
// ============================================
// Valid data types for property values
// Determines how Property.value is interpreted
Enum property_value_type {
// Valid data types for custom field values
// Determines how CustomFieldValue.value is interpreted
Enum custom_field_value_type {
string [note: 'Text data']
integer [note: 'Numeric data']
boolean [note: 'True/False flags']
@ -335,8 +335,8 @@ TableGroup accounts_domain {
TableGroup membership_domain {
members
properties
property_types
custom_field_values
custom_fields
Note: '''
**Membership Domain**

View file

@ -131,11 +131,11 @@ Based on closed PRs from https://git.local-it.org/local-it/mitgliederverwaltung/
**Sprint 3 - 28.05 - 09.07**
- Member CRUD operations
- Basic property system
- Basic custom field system
- Initial UI with Tailwind CSS
**Sprint 4 - 09.07 - 30.07**
- Property types implementation
- CustomFieldValue types implementation
- Data validation
- Error handling improvements
@ -154,7 +154,7 @@ Based on closed PRs from https://git.local-it.org/local-it/mitgliederverwaltung/
**PR #147:** *Add seed data for members*
- Comprehensive seed data
- Test users and members
- Property type examples
- CustomFieldValue type examples
#### Phase 3: Search & Navigation (Sprint 6)
@ -379,21 +379,21 @@ end
**Complete documentation:** See [`docs/email-sync.md`](email-sync.md) for decision tree and sync rules.
#### 4. Property System (EAV Pattern)
#### 4. CustomFieldValue System (EAV Pattern)
**Implementation:** Entity-Attribute-Value pattern with union types
```elixir
# Property Type defines schema
defmodule Mv.Membership.PropertyType do
# CustomFieldValue Type defines schema
defmodule Mv.Membership.CustomField do
attribute :name, :string # "Membership Number"
attribute :value_type, :atom # :string, :integer, :boolean, :date, :email
attribute :immutable, :boolean # Can't change after creation
attribute :required, :boolean # All members must have this
end
# Property stores values
defmodule Mv.Membership.Property do
# CustomFieldValue stores values
defmodule Mv.Membership.CustomFieldValue do
attribute :value, :union, # Polymorphic value storage
constraints: [
types: [
@ -405,7 +405,7 @@ defmodule Mv.Membership.Property do
]
]
belongs_to :member
belongs_to :property_type
belongs_to :custom_field
end
```
@ -413,12 +413,12 @@ end
- Clubs need different custom fields
- No schema migrations for new fields
- Type safety with union types
- Centralized property management
- Centralized custom field management
**Constraints:**
- One property per type per member (composite unique index)
- One custom field value per custom field per member (composite unique index)
- Properties deleted with member (CASCADE)
- Property types protected if in use (RESTRICT)
- CustomFieldValue types protected if in use (RESTRICT)
#### 5. Authentication Strategy
@ -593,7 +593,7 @@ end
#### Database Migrations
**Key migrations in chronological order:**
1. `20250528163901_initial_migration.exs` - Core tables (members, properties, property_types)
1. `20250528163901_initial_migration.exs` - Core tables (members, custom_field_values, custom_fields)
2. `20250617090641_member_fields.exs` - Member attributes expansion
3. `20250620110850_add_accounts_domain.exs` - Users & tokens tables
4. `20250912085235_AddSearchVectorToMembers.exs` - Full-text search (tsvector + GIN index)
@ -772,7 +772,7 @@ end
- Admin user: `admin@mv.local` / `testpassword`
- Sample members: Hans Müller, Greta Schmidt, Friedrich Wagner
- Linked accounts: Maria Weber, Thomas Klein
- Property types: String, Date, Boolean, Email
- CustomFieldValue types: String, Date, Boolean, Email
**Test Helpers:**
```elixir
@ -956,9 +956,9 @@ mix credo --strict
mix credo suggest --format=oneline
```
### 8. Property Value Type Mismatch
### 8. CustomFieldValue Value Type Mismatch
**Issue:** Property value doesn't match property_type definition.
**Issue:** CustomFieldValue value doesn't match custom_field definition.
**Error:**
```
@ -966,16 +966,16 @@ mix credo suggest --format=oneline
```
**Solution:**
Ensure property value matches property_type.value_type:
Ensure custom field value matches custom_field.value_type:
```elixir
# Property Type: value_type = :integer
property_type = get_property_type("age")
# CustomFieldValue Type: value_type = :integer
custom_field = get_custom_field("age")
# Property Value: must be integer union type
{:ok, property} = create_property(%{
# CustomFieldValue Value: must be integer union type
{:ok, custom_field_value} = create_custom_field_value(%{
value: %{type: :integer, value: 25}, # Not "25" as string
property_type_id: property_type.id
custom_field_id: custom_field.id
})
```

View file

@ -87,12 +87,12 @@
---
#### 3. **Custom Fields (Property System)** 🔧
#### 3. **Custom Fields (CustomFieldValue System)** 🔧
**Current State:**
- ✅ Property types (string, integer, boolean, date, email)
- ✅ Property type management
- ✅ Dynamic property assignment to members
- ✅ CustomFieldValue types (string, integer, boolean, date, email)
- ✅ CustomFieldValue type management
- ✅ Dynamic custom field value assignment to members
- ✅ Union type storage (JSONB)
**Open Issues:**
@ -217,7 +217,7 @@
- ❌ Global settings management
- ❌ Club/Organization profile
- ❌ Email templates configuration
- ❌ Property type management UI (user-facing)
- ❌ CustomFieldValue type management UI (user-facing)
- ❌ Role and permission management UI
- ❌ System health dashboard
- ❌ Audit log viewer
@ -481,9 +481,9 @@ Since this is a **Phoenix LiveView** application with **Ash Framework**, we have
| Mount | Purpose | Auth | Query Params | Events |
|-------|---------|------|--------------|--------|
| `/members` | Member list with search/sort | 🔐 | `?search=&sort_by=&sort_dir=` | `search`, `sort`, `delete`, `select` |
| `/members/new` | Create new member form | 🔐 | - | `save`, `cancel`, `add_property` |
| `/members/new` | Create new member form | 🔐 | - | `save`, `cancel`, `add_custom_field_value` |
| `/members/:id` | Member detail view | 🔐 | - | `edit`, `delete`, `link_user` |
| `/members/:id/edit` | Edit member form | 🔐 | - | `save`, `cancel`, `add_property`, `remove_property` |
| `/members/:id/edit` | Edit member form | 🔐 | - | `save`, `cancel`, `add_custom_field_value`, `remove_custom_field_value` |
#### LiveView Event Handlers
@ -495,8 +495,8 @@ Since this is a **Phoenix LiveView** application with **Ash Framework**, we have
| `save` | Create/update member | `%{"member" => attrs}` | Redirect or show errors |
| `link_user` | Link user to member | `%{"user_id" => id}` | Update member view |
| `unlink_user` | Unlink user from member | - | Update member view |
| `add_property` | Add custom property | `%{"property_type_id" => id, "value" => val}` | Update form |
| `remove_property` | Remove custom property | `%{"property_id" => id}` | Update form |
| `add_custom_field_value` | Add custom field value | `%{"custom_field_id" => id, "value" => val}` | Update form |
| `remove_custom_field_value` | Remove custom field value | `%{"custom_field_value_id" => id}` | Update form |
#### Ash Resource Actions
@ -517,7 +517,7 @@ Since this is a **Phoenix LiveView** application with **Ash Framework**, we have
| `Member` | `:fuzzy_search` | Fuzzy text search | 🔐 | `{query, threshold}` | `[%Member{}]` |
| `Member` | `:advanced_search` | Multi-criteria search | 🔐 | `{filters: [{field, op, value}]}` | `[%Member{}]` |
| `Member` | `:paginate` | Paginated member list | 🔐 | `{page, per_page, filters}` | `{members, total, page_info}` |
| `Member` | `:sort_by_custom_field` | Sort by property | 🔐 | `{property_type_id, direction}` | `[%Member{}]` |
| `Member` | `:sort_by_custom_field` | Sort by custom field | 🔐 | `{custom_field_id, direction}` | `[%Member{}]` |
| `Member` | `:bulk_delete` | Delete multiple members | 🛡️ | `{ids: [id1, id2, ...]}` | `{:ok, count}` |
| `Member` | `:bulk_update` | Update multiple members | 🛡️ | `{ids, attrs}` | `{:ok, count}` |
| `Member` | `:export` | Export to CSV/Excel | 🔐 | `{format, filters}` | File download |
@ -525,37 +525,37 @@ Since this is a **Phoenix LiveView** application with **Ash Framework**, we have
---
### 3. Custom Fields (Property System) Endpoints
### 3. Custom Fields (CustomFieldValue System) Endpoints
#### LiveView Endpoints
| Mount | Purpose | Auth | Events |
|-------|---------|------|--------|
| `/property-types` | List property types | 🛡️ | `new`, `edit`, `delete` |
| `/property-types/new` | Create property type | 🛡️ | `save`, `cancel` |
| `/property-types/:id/edit` | Edit property type | 🛡️ | `save`, `cancel`, `delete` |
| `/custom-fields` | List custom fields | 🛡️ | `new`, `edit`, `delete` |
| `/custom-fields/new` | Create custom field | 🛡️ | `save`, `cancel` |
| `/custom-fields/:id/edit` | Edit custom field | 🛡️ | `save`, `cancel`, `delete` |
#### Ash Resource Actions
| Resource | Action | Purpose | Auth | Input | Output |
|----------|--------|---------|------|-------|--------|
| `PropertyType` | `:create` | Create property type | 🛡️ | `{name, value_type, description, ...}` | `{:ok, property_type}` |
| `PropertyType` | `:read` | List property types | 🔐 | - | `[%PropertyType{}]` |
| `PropertyType` | `:update` | Update property type | 🛡️ | `{id, attrs}` | `{:ok, property_type}` |
| `PropertyType` | `:destroy` | Delete property type | 🛡️ | `{id}` | `{:ok, property_type}` |
| `Property` | `:create` | Add property to member | 🔐 | `{member_id, property_type_id, value}` | `{:ok, property}` |
| `Property` | `:update` | Update property value | 🔐 | `{id, value}` | `{:ok, property}` |
| `Property` | `:destroy` | Remove property | 🔐 | `{id}` | `{:ok, property}` |
| `CustomField` | `:create` | Create custom field | 🛡️ | `{name, value_type, description, ...}` | `{:ok, custom_field}` |
| `CustomField` | `:read` | List custom fields | 🔐 | - | `[%CustomField{}]` |
| `CustomField` | `:update` | Update custom field | 🛡️ | `{id, attrs}` | `{:ok, custom_field}` |
| `CustomField` | `:destroy` | Delete custom field | 🛡️ | `{id}` | `{:ok, custom_field}` |
| `CustomFieldValue` | `:create` | Add custom field value to member | 🔐 | `{member_id, custom_field_id, value}` | `{:ok, custom_field_value}` |
| `CustomFieldValue` | `:update` | Update custom field value | 🔐 | `{id, value}` | `{:ok, custom_field_value}` |
| `CustomFieldValue` | `:destroy` | Remove custom field value | 🔐 | `{id}` | `{:ok, custom_field_value}` |
#### **NEW: Enhanced Custom Fields** (Issue #194, #157, #161, #153)
| Resource | Action | Purpose | Auth | Input | Output |
|----------|--------|---------|------|-------|--------|
| `PropertyType` | `:set_default_visibility` | Show/hide by default | 🛡️ | `{id, visible}` | `{:ok, property_type}` |
| `PropertyType` | `:set_required` | Mark as required | 🛡️ | `{id, required}` | `{:ok, property_type}` |
| `PropertyType` | `:add_validation` | Add validation rule | 🛡️ | `{id, rule_type, params}` | `{:ok, property_type}` |
| `PropertyType` | `:create_group` | Create field group | 🛡️ | `{name, property_type_ids}` | `{:ok, group}` |
| `Property` | `:validate_value` | Validate property value | 🔐 | `{property_type_id, value}` | `{:ok, valid}` or `{:error, reason}` |
| `CustomField` | `:set_default_visibility` | Show/hide by default | 🛡️ | `{id, visible}` | `{:ok, custom_field}` |
| `CustomField` | `:set_required` | Mark as required | 🛡️ | `{id, required}` | `{:ok, custom_field}` |
| `CustomField` | `:add_validation` | Add validation rule | 🛡️ | `{id, rule_type, params}` | `{:ok, custom_field}` |
| `CustomField` | `:create_group` | Create field group | 🛡️ | `{name, custom_field_ids}` | `{:ok, group}` |
| `CustomFieldValue` | `:validate_value` | Validate custom field value | 🔐 | `{custom_field_id, value}` | `{:ok, valid}` or `{:error, reason}` |
---