Add groups resource close #371 #378

Merged
simon merged 7 commits from feature/371-groups-resource into main 2026-01-27 17:17:26 +01:00
Showing only changes of commit 2ebf289112 - Show all commits

View file

@ -49,7 +49,8 @@ This document defines the technical architecture for the Groups feature. It focu
- No parent/child relationships in MVP
3. **Minimal Attributes (MVP):**
- Only `name` and `description` in initial version
- `name`, `description`, and `slug` in initial version
- `slug` is automatically generated from `name` (immutable, URL-friendly)
- Extensible for future attributes (dates, status, etc.)
4. **Cascade Deletion:**
@ -71,7 +72,7 @@ This document defines the technical architecture for the Groups feature. It focu
**New Resources:**
- `Group` - Group definitions (name, description)
- `Group` - Group definitions (name, description, slug)
- `MemberGroup` - Join table for many-to-many relationship between Members and Groups
**Extended Resources:**
@ -113,13 +114,17 @@ lib/
**Attributes:**
- `id` - UUID v7 primary key
- `name` - Unique group name (required, max 100 chars)
- `slug` - URL-friendly identifier (required, max 100 chars, auto-generated from name)
- `description` - Optional description (max 500 chars)
- `inserted_at` / `updated_at` - Timestamps
**Constraints:**
- `name` must be unique (case-insensitive, using LOWER(name))
- `slug` must be unique (case-sensitive, exact match)
- `name` cannot be null
- `slug` cannot be null
- `name` max length: 100 characters
- `slug` max length: 100 characters
- `description` max length: 500 characters
#### `member_groups` Table (Join Table)
@ -151,15 +156,19 @@ lib/
- `member_count` - Integer calculation counting associated members
**Actions:**
- `create` - Create new group
- `read` - List/search groups
- `update` - Update group name/description
- `create` - Create new group (auto-generates slug from name)
- `read` - List/search groups (can query by slug via identity)
- `update` - Update group name/description (slug remains unchanged)
- `destroy` - Delete group (with confirmation)
**Validations:**
- `name` required, unique (case-insensitive), max 100 chars
- `slug` required, unique (case-sensitive), max 100 chars, auto-generated, immutable
- `description` optional, max 500 chars
**Identities:**
- `unique_slug` - Unique identity on `slug` for efficient lookups
#### `Mv.Membership.MemberGroup`
**Relationships:**
@ -192,12 +201,14 @@ lib/
**Create Group:**
- Validate name uniqueness
- Generate slug (if needed for future URL-friendly identifiers)
- Automatically generate slug from name (using `GenerateSlug` change, same pattern as CustomFields)
- Validate slug uniqueness
- Return created group
**Update Group:**
- Validate name uniqueness (if name changed)
- Update description
- Slug remains unchanged (immutable after creation)
- Return updated group
**Delete Group:**
@ -295,7 +306,7 @@ lib/
### Group Detail View (`/groups/:id`)
**Route:** `/groups/:id` - Group detail page
**Route:** `/groups/:id` - Group detail page (uses UUID, slug can be used for future `/groups/:slug` routes)
**Features:**
- Display group name and description
@ -304,6 +315,8 @@ lib/
- Edit group button
- Delete group button (with confirmation)
**Note:** Currently uses UUID for routing. Slug is available for future URL-friendly routes (`/groups/:slug`).
### Accessibility (A11y) Considerations
**Requirements:**
@ -431,7 +444,8 @@ lib/
### Database Indexes
**Critical Indexes:**
- `groups.name` - For uniqueness and search
- `groups.name` - For uniqueness and search (case-insensitive via LOWER)
- `groups.slug` - For uniqueness and efficient lookups (unique index)
- `member_groups.member_id` - For member → groups queries
- `member_groups.group_id` - For group → members queries
- Composite index on `(member_id, group_id)` - For uniqueness check
@ -440,7 +454,7 @@ lib/
**Member Overview:**
- Load groups with members in single query using query preloading
- Preload only necessary group attributes (id, name) to minimize data transfer
- Preload only necessary group attributes (id, name, slug) to minimize data transfer
- Filter groups at database level when filtering by group
**N+1 Query Prevention:**
@ -531,7 +545,7 @@ The Groups feature is divided into **functionally complete, vertical units**. Ea
**Minimal Viable Product (MVP):**
The MVP includes the **core functionality** necessary to manage groups and assign them to members:
1. ✅ Create groups (Name + Description)
1. ✅ Create groups (Name + Description + Slug)
2. ✅ Edit groups
3. ✅ Delete groups (with confirmation)
4. ✅ Assign members to groups
@ -554,9 +568,10 @@ The MVP includes the **core functionality** necessary to manage groups and assig
**Functional Scope:** Administrators can manage groups in the system
**Scope:**
- Group resource (Name, Description)
- Group resource (Name, Description, Slug)
- CRUD operations for groups
- Validations (unique name, length limits)
- Validations (unique name, unique slug, length limits)
- Automatic slug generation from name
- Delete logic with cascade behavior
**Deliverable:** Groups can be created, edited, and deleted via API
@ -731,12 +746,13 @@ Each functional unit can be implemented as a **separate issue**:
**Goal:** Basic group management and member assignment
**Tasks:**
1. Create `Group` resource (name, description)
2. Create `MemberGroup` join table resource
3. Extend `Member` with groups relationship
4. Database migrations
5. Basic CRUD actions for groups
6. Add/remove members from groups (via group management)
1. Create `Group` resource (name, description, slug)
2. Implement slug generation (reuse `GenerateSlug` change from CustomFields)
3. Create `MemberGroup` join table resource
4. Extend `Member` with groups relationship
5. Database migrations (including slug column and unique index)
6. Basic CRUD actions for groups
7. Add/remove members from groups (via group management)
**Deliverables:**
- Groups can be created, edited, deleted
@ -841,16 +857,18 @@ Each functional unit can be implemented as a **separate issue**:
**Type:** Backend
**Estimation:** 4-5h
**Tasks:**
- Create `Group` resource
- Create `Group` resource (with slug attribute and generation)
- Create `MemberGroup` join table resource
- Extend `Member` resource
- Database migrations
- Basic validations
- Database migrations (including slug column)
- Basic validations (name, slug, description)
**Acceptance Criteria:**
- Groups can be created via Ash API
- Slug is automatically generated from name
- Slug is unique and immutable
- Members can be associated with groups
- Database constraints enforced
- Database constraints enforced (unique name, unique slug, foreign keys)
### Issue 2: Groups Management UI
**Type:** Frontend
@ -941,8 +959,19 @@ Each functional unit can be implemented as a **separate issue**:
- Allow description to be nil
- Trim whitespace from name
- Description max length is 500 characters
- Slug is automatically generated from name on create
- Slug is immutable (doesn't change on update)
- Slug is unique (cannot have duplicates)
- Slug handles German umlauts correctly (ä → a, ß → ss)
- Slug converts to lowercase
- Slug removes special characters
- Slug replaces spaces with hyphens
- Slug truncates to max 100 characters
- Slug cannot be empty (validation fails if slug would be empty)
- Read group by slug (via identity lookup)
- Update group name and description
- Prevent duplicate name on update
- Prevent duplicate slug on create (same name → same slug → error)
- Delete group and all member associations
- Do not delete members themselves
- Member count calculation returns 0 for empty group
@ -997,11 +1026,18 @@ Each functional unit can be implemented as a **separate issue**:
**Migration 1: Create groups table**
- Create table with UUID v7 primary key
- Add name field (required, unique, case-insensitive)
- Add slug field (required, unique, case-sensitive, auto-generated)
- Add description field (optional)
- Add timestamps
- Create unique index on lowercased name
- Create unique index on lowercased name (for name uniqueness)
- Create unique index on slug (for slug uniqueness and lookups)
- Create index on lowercased name for search
**Note:** Slug generation follows the same pattern as CustomFields:
- Uses `Mv.Membership.CustomField.Changes.GenerateSlug` (reusable change)
- Or create `Mv.Membership.Group.Changes.GenerateSlug` if needed
- Slug is generated on create, immutable on update
**Migration 2: Create member_groups join table**
- Create table with UUID v7 primary key
- Add member_id and group_id foreign keys
@ -1030,4 +1066,13 @@ Each functional unit can be implemented as a **separate issue**:
This architecture provides a solid foundation for the Groups feature while maintaining flexibility for future enhancements. The many-to-many relationship is implemented via a join table, following existing patterns in the codebase. The MVP focuses on core functionality (create, edit, delete groups, assign members) with clear extension points for hierarchical groups, roles, and advanced permissions.
**Slug Implementation:**
Groups include automatic slug generation, following the same pattern as CustomFields. Slugs are:
- Automatically generated from the `name` attribute on create
- Immutable after creation (don't change when name is updated)
- Unique and URL-friendly
- Available for future route enhancements (e.g., `/groups/:slug` instead of `/groups/:id`)
The implementation reuses the existing `GenerateSlug` change from CustomFields, ensuring consistency across the codebase.
The implementation is split into 6 manageable issues, totaling approximately 15 hours of work, aligning with the original estimation. Each phase builds on the previous one, allowing for incremental development and testing.