feat: add tenant and user management with migrations, handlers, and tests
- Implemented tenant and user management features including: - Tenant creation and management with associated migrations. - User creation and management with associated migrations. - Tenant membership management with associated migrations. - Platform user roles management with associated migrations. - Quota management with associated migrations. - Article and template management with associated migrations. - Added HTTP handlers for templates and workspaces. - Created tests for protected and public routes. - Introduced a script to check tenant scope in SQL queries. - Documented task plan for backend completion and frontend foundation.
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GenerationEvent struct {
|
||||
Type string `json:"type"`
|
||||
ArticleID int64 `json:"article_id"`
|
||||
TaskID int64 `json:"task_id"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Delta string `json:"delta,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Done bool `json:"done,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type generationStream struct {
|
||||
snapshot GenerationEvent
|
||||
subscribers map[int]chan GenerationEvent
|
||||
nextSubscriber int
|
||||
completed bool
|
||||
}
|
||||
|
||||
type GenerationHub struct {
|
||||
mu sync.Mutex
|
||||
streams map[int64]*generationStream
|
||||
}
|
||||
|
||||
func NewGenerationHub() *GenerationHub {
|
||||
return &GenerationHub{
|
||||
streams: make(map[int64]*generationStream),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *GenerationHub) Start(articleID, taskID int64, title string) {
|
||||
h.publish(articleID, func(state *generationStream) GenerationEvent {
|
||||
now := time.Now()
|
||||
state.completed = false
|
||||
state.snapshot = GenerationEvent{
|
||||
Type: "snapshot",
|
||||
ArticleID: articleID,
|
||||
TaskID: taskID,
|
||||
Title: title,
|
||||
Status: "generating",
|
||||
UpdatedAt: now,
|
||||
}
|
||||
return GenerationEvent{
|
||||
Type: "status",
|
||||
ArticleID: articleID,
|
||||
TaskID: taskID,
|
||||
Title: title,
|
||||
Status: "generating",
|
||||
UpdatedAt: now,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (h *GenerationHub) AppendDelta(articleID, taskID int64, title, delta string) {
|
||||
h.publish(articleID, func(state *generationStream) GenerationEvent {
|
||||
now := time.Now()
|
||||
state.snapshot.Type = "snapshot"
|
||||
state.snapshot.ArticleID = articleID
|
||||
state.snapshot.TaskID = taskID
|
||||
state.snapshot.Title = title
|
||||
state.snapshot.Status = "generating"
|
||||
state.snapshot.Content += delta
|
||||
state.snapshot.UpdatedAt = now
|
||||
|
||||
return GenerationEvent{
|
||||
Type: "delta",
|
||||
ArticleID: articleID,
|
||||
TaskID: taskID,
|
||||
Title: title,
|
||||
Status: "generating",
|
||||
Delta: delta,
|
||||
Content: state.snapshot.Content,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (h *GenerationHub) Complete(articleID, taskID int64, title, content string) {
|
||||
h.publish(articleID, func(state *generationStream) GenerationEvent {
|
||||
now := time.Now()
|
||||
state.completed = true
|
||||
state.snapshot = GenerationEvent{
|
||||
Type: "snapshot",
|
||||
ArticleID: articleID,
|
||||
TaskID: taskID,
|
||||
Title: title,
|
||||
Status: "completed",
|
||||
Content: content,
|
||||
Done: true,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
return GenerationEvent{
|
||||
Type: "completed",
|
||||
ArticleID: articleID,
|
||||
TaskID: taskID,
|
||||
Title: title,
|
||||
Status: "completed",
|
||||
Content: content,
|
||||
Done: true,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (h *GenerationHub) Fail(articleID, taskID int64, title, errMsg string) {
|
||||
h.publish(articleID, func(state *generationStream) GenerationEvent {
|
||||
now := time.Now()
|
||||
state.completed = true
|
||||
state.snapshot.Type = "snapshot"
|
||||
state.snapshot.ArticleID = articleID
|
||||
state.snapshot.TaskID = taskID
|
||||
state.snapshot.Title = title
|
||||
state.snapshot.Status = "failed"
|
||||
state.snapshot.Error = errMsg
|
||||
state.snapshot.Done = true
|
||||
state.snapshot.UpdatedAt = now
|
||||
|
||||
return GenerationEvent{
|
||||
Type: "error",
|
||||
ArticleID: articleID,
|
||||
TaskID: taskID,
|
||||
Title: title,
|
||||
Status: "failed",
|
||||
Error: errMsg,
|
||||
Done: true,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (h *GenerationHub) Subscribe(articleID int64) (<-chan GenerationEvent, *GenerationEvent, func()) {
|
||||
h.mu.Lock()
|
||||
state := h.streams[articleID]
|
||||
if state == nil {
|
||||
h.mu.Unlock()
|
||||
return nil, nil, func() {}
|
||||
}
|
||||
|
||||
id := state.nextSubscriber
|
||||
state.nextSubscriber++
|
||||
|
||||
ch := make(chan GenerationEvent, 64)
|
||||
state.subscribers[id] = ch
|
||||
snapshot := state.snapshot
|
||||
h.mu.Unlock()
|
||||
|
||||
cancel := func() {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
current := h.streams[articleID]
|
||||
if current == nil {
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
|
||||
delete(current.subscribers, id)
|
||||
shouldDelete := current.completed && len(current.subscribers) == 0
|
||||
if shouldDelete {
|
||||
delete(h.streams, articleID)
|
||||
}
|
||||
|
||||
close(ch)
|
||||
}
|
||||
|
||||
return ch, &snapshot, cancel
|
||||
}
|
||||
|
||||
func (h *GenerationHub) Snapshot(articleID int64) (*GenerationEvent, bool) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
state := h.streams[articleID]
|
||||
if state == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
snapshot := state.snapshot
|
||||
return &snapshot, true
|
||||
}
|
||||
|
||||
func (h *GenerationHub) publish(articleID int64, mutate func(*generationStream) GenerationEvent) {
|
||||
h.mu.Lock()
|
||||
state := h.ensureLocked(articleID)
|
||||
event := mutate(state)
|
||||
|
||||
subscribers := make([]chan GenerationEvent, 0, len(state.subscribers))
|
||||
for _, ch := range state.subscribers {
|
||||
subscribers = append(subscribers, ch)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
for _, ch := range subscribers {
|
||||
select {
|
||||
case ch <- event:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *GenerationHub) ensureLocked(articleID int64) *generationStream {
|
||||
state := h.streams[articleID]
|
||||
if state != nil {
|
||||
return state
|
||||
}
|
||||
|
||||
state = &generationStream{
|
||||
subscribers: make(map[int]chan GenerationEvent),
|
||||
}
|
||||
h.streams[articleID] = state
|
||||
return state
|
||||
}
|
||||
Reference in New Issue
Block a user