rdev/internal/handlers/conversation_dto.go
jordan a69eb7e587 feat(foundary): implement complete backend for conversational project design
Implements all 5 phases of Foundary Studio backend:

Phase 1: Chat Persistence (8 API endpoints)
- Conversations and messages with proper cascading deletes
- PostgreSQL schema with auto-update triggers
- Full CRUD operations with structured logging

Phase 2: Blueprint Entity (5 API endpoints)
- JSONB spec storage with GIN indexes
- Flexible structured data for project specifications
- Version-controlled blueprint management

Phase 3: Architect Service (3 API endpoints)
- Conversational AI orchestration with Claude
- Multi-turn dialogue with context building
- Blueprint spec extraction from conversations

Phase 4: Work Queue Integration
- Verified existing endpoint compatibility

Phase 5: Structured Questions (6 API endpoints)
- Four question types: text, choice, multichoice, yesno
- Answer validation with proper constraints
- Conversation-linked Q&A flow

Architecture:
- Textbook hexagonal architecture (domain → port → adapter → service → handler)
- Zero external dependencies in domain layer
- Consistent error handling with proper wrapping
- Auth scopes on all routes (projects:read, projects:execute)
- Structured logging with operation context and duration tracking
- NULL-safe DTO converters throughout

Database:
- 3 new migrations (019, 020, 021)
- UUIDs for all primary keys
- Proper foreign key constraints with ON DELETE CASCADE
- Optimized indexes including partial index for unanswered questions
- Auto-update triggers for timestamps

OpenAPI Documentation:
- Complete API documentation under 'Foundary' tag
- 22 new endpoints documented with examples
- Request/response schemas for all operations

Logging Improvements:
- Added operation field to all service logs
- Added duration_ms tracking for performance monitoring
- Log response_length instead of full response content
- Consistent use of logging field constants
- Execute-then-log pattern for delete operations

Files: 32 changed, 2800+ lines added
- 7 domain models
- 3 database migrations
- 3 port interfaces
- 3 postgres adapters
- 4 services (conversation, blueprint, question, architect)
- 4 handlers with DTOs
- OpenAPI documentation
- Integration in main.go

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-09 00:50:46 -07:00

77 lines
2.0 KiB
Go

package handlers
import "github.com/orchard9/rdev/internal/domain"
// ConversationDTO is the data transfer object for conversations.
type ConversationDTO struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
Title string `json:"title"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
LastMessageAt *string `json:"last_message_at,omitempty"`
}
// MessageDTO is the data transfer object for messages.
type MessageDTO struct {
ID string `json:"id"`
ConversationID string `json:"conversation_id"`
Role string `json:"role"`
Content string `json:"content"`
CreatedAt string `json:"created_at"`
}
// ConversationWithMessagesDTO combines a conversation with its messages.
type ConversationWithMessagesDTO struct {
ConversationDTO
Messages []*MessageDTO `json:"messages"`
}
func toConversationDTO(c *domain.Conversation) *ConversationDTO {
if c == nil {
return nil
}
dto := &ConversationDTO{
ID: string(c.ID),
ProjectID: c.ProjectID,
Title: c.Title,
CreatedAt: c.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: c.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if c.LastMessage != nil {
ts := c.LastMessage.Format("2006-01-02T15:04:05Z07:00")
dto.LastMessageAt = &ts
}
return dto
}
func toMessageDTO(m *domain.Message) *MessageDTO {
if m == nil {
return nil
}
return &MessageDTO{
ID: string(m.ID),
ConversationID: string(m.ConversationID),
Role: string(m.Role),
Content: m.Content,
CreatedAt: m.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
func toConversationWithMessagesDTO(c *domain.ConversationWithMessages) *ConversationWithMessagesDTO {
if c == nil {
return nil
}
messages := make([]*MessageDTO, len(c.Messages))
for i, m := range c.Messages {
messages[i] = toMessageDTO(m)
}
dto := toConversationDTO(&c.Conversation)
return &ConversationWithMessagesDTO{
ConversationDTO: *dto,
Messages: messages,
}
}