add model identities and restore httpx router

This commit is contained in:
2026-07-19 00:06:21 +08:00
parent f7cd69c70f
commit 4e46fee809
20 changed files with 468 additions and 123 deletions

View File

@@ -3,14 +3,16 @@ package models
import "time"
type SenlinAgentAICallLog struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"index;not null"`
Provider string `gorm:"not null"`
UsedKeyType string `gorm:"not null"`
Action string `gorm:"not null"`
Status string `gorm:"not null"`
Error string
CreatedAt time.Time
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
UserID uint `gorm:"index;not null"`
UserIdentity string `gorm:"type:char(36);index"`
Provider string `gorm:"not null"`
UsedKeyType string `gorm:"not null"`
Action string `gorm:"not null"`
Status string `gorm:"not null"`
Error string
CreatedAt time.Time
}
func (SenlinAgentAICallLog) TableName() string {

View File

@@ -4,7 +4,9 @@ import "time"
type SenlinAgentAIKey struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
UserID uint `gorm:"uniqueIndex;not null"`
UserIdentity string `gorm:"type:char(36);index"`
Provider string `gorm:"not null"`
EncryptedAPIKey string `gorm:"not null"`
CreatedAt time.Time

View File

@@ -3,13 +3,16 @@ package models
import "time"
type SenlinAgentAISession struct {
ID uint `gorm:"primaryKey"`
ProjectID uint `gorm:"index;not null"`
CreatedBy uint `gorm:"index;not null"`
Title string `gorm:"not null"`
Context string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
ProjectIdentity string `gorm:"type:char(36);index"`
CreatedBy uint `gorm:"index;not null"`
CreatedByIdentity string `gorm:"type:char(36);index"`
Title string `gorm:"not null"`
Context string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (SenlinAgentAISession) TableName() string {

View File

@@ -0,0 +1,202 @@
package models
import (
"database/sql"
"github.com/google/uuid"
"gorm.io/gorm"
)
func (m *SenlinAgentUser) BeforeCreate(tx *gorm.DB) error {
return ensureIdentity(&m.Identity)
}
func (m *SenlinAgentProject) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentUser{}, m.OwnerID, &m.OwnerIdentity)
}
func (m *SenlinAgentInboxItem) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity)
}
func (m *SenlinAgentTask) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
return err
}
if err := resolveOptionalIdentity(tx, &SenlinAgentUser{}, m.AssigneeID, &m.AssigneeIdentity); err != nil {
return err
}
return resolveOptionalIdentity(tx, &SenlinAgentInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
}
func (m *SenlinAgentNote) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
return err
}
return resolveOptionalIdentity(tx, &SenlinAgentInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
}
func (m *SenlinAgentSource) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
return err
}
return resolveOptionalIdentity(tx, &SenlinAgentInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
}
func (m *SenlinAgentAISession) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity)
}
func (m *SenlinAgentTag) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity)
}
func (m *SenlinAgentProjectEvent) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.ActorID, &m.ActorIdentity); err != nil {
return err
}
return resolveEntityIdentity(tx, m.EntityType, m.EntityID, &m.EntityIdentity)
}
func (m *SenlinAgentAIKey) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentUser{}, m.UserID, &m.UserIdentity)
}
func (m *SenlinAgentAICallLog) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
return resolveIdentity(tx, &SenlinAgentUser{}, m.UserID, &m.UserIdentity)
}
func (m *SenlinAgentTaskShare) BeforeCreate(tx *gorm.DB) error {
if err := ensureIdentity(&m.Identity); err != nil {
return err
}
if err := resolveIdentity(tx, &SenlinAgentTask{}, m.TaskID, &m.TaskIdentity); err != nil {
return err
}
return resolveEntityIdentity(tx, m.ObjectType, m.ObjectID, &m.ObjectIdentity)
}
func ensureIdentity(identity *string) error {
if *identity != "" {
return nil
}
next, err := uuidV7()
if err != nil {
return err
}
*identity = next
return nil
}
func resolveIdentity(tx *gorm.DB, model any, id uint, identity *string) error {
if id == 0 || *identity != "" {
return nil
}
resolved, err := lookupIdentity(tx, model, id)
if err != nil {
return err
}
*identity = resolved
return nil
}
func resolveOptionalIdentity(tx *gorm.DB, model any, id *uint, identity **string) error {
if id == nil || *id == 0 || (identity != nil && *identity != nil && **identity != "") {
return nil
}
resolved, err := lookupIdentity(tx, model, *id)
if err != nil {
return err
}
*identity = &resolved
return nil
}
func resolveEntityIdentity(tx *gorm.DB, entityType string, entityID uint, identity *string) error {
if entityID == 0 || *identity != "" {
return nil
}
switch entityType {
case "project":
return resolveIdentity(tx, &SenlinAgentProject{}, entityID, identity)
case "inbox", "inbox_item":
return resolveIdentity(tx, &SenlinAgentInboxItem{}, entityID, identity)
case "task":
return resolveIdentity(tx, &SenlinAgentTask{}, entityID, identity)
case "note":
return resolveIdentity(tx, &SenlinAgentNote{}, entityID, identity)
case "source":
return resolveIdentity(tx, &SenlinAgentSource{}, entityID, identity)
case "ai_session":
return resolveIdentity(tx, &SenlinAgentAISession{}, entityID, identity)
case "tag":
return resolveIdentity(tx, &SenlinAgentTag{}, entityID, identity)
default:
return nil
}
}
func lookupIdentity(tx *gorm.DB, model any, id uint) (string, error) {
var identity string
err := tx.Model(model).Select("identity").Where("id = ?", id).Row().Scan(&identity)
if err == sql.ErrNoRows {
return "", nil
}
return identity, err
}
func uuidV7() (string, error) {
next, err := uuid.NewV7()
if err != nil {
return "", err
}
return next.String(), nil
}

View File

@@ -0,0 +1,49 @@
package models
import (
"fmt"
"testing"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func TestBeforeCreateAssignsUUIDV7Identity(t *testing.T) {
database := newIdentityTestDB(t)
user := SenlinAgentUser{Email: "lead@example.com", DisplayName: "Lead", PasswordHash: "hash"}
require.NoError(t, database.Create(&user).Error)
require.NotEmpty(t, user.Identity)
require.Equal(t, byte('7'), user.Identity[14])
}
func TestBeforeCreateCopiesParentIdentitiesFromNumericIDs(t *testing.T) {
database := newIdentityTestDB(t)
user := SenlinAgentUser{Email: "lead@example.com", DisplayName: "Lead", PasswordHash: "hash"}
require.NoError(t, database.Create(&user).Error)
project := SenlinAgentProject{OwnerID: user.ID, Name: "Alpha"}
require.NoError(t, database.Create(&project).Error)
item := SenlinAgentInboxItem{ProjectID: project.ID, CreatedBy: user.ID, SourceType: "text"}
require.NoError(t, database.Create(&item).Error)
task := SenlinAgentTask{ProjectID: project.ID, CreatedBy: user.ID, SourceInboxItemID: &item.ID, Title: "Follow up"}
require.NoError(t, database.Create(&task).Error)
require.NotEmpty(t, project.Identity)
require.Equal(t, user.Identity, project.OwnerIdentity)
require.Equal(t, project.Identity, task.ProjectIdentity)
require.Equal(t, user.Identity, task.CreatedByIdentity)
require.NotNil(t, task.SourceInboxItemIdentity)
require.Equal(t, item.Identity, *task.SourceInboxItemIdentity)
}
func newIdentityTestDB(t *testing.T) *gorm.DB {
t.Helper()
database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, AutoMigrate(database))
return database
}

View File

@@ -3,15 +3,18 @@ package models
import "time"
type SenlinAgentInboxItem struct {
ID uint `gorm:"primaryKey"`
ProjectID uint `gorm:"index;not null"`
CreatedBy uint `gorm:"index;not null"`
SourceType string `gorm:"not null"`
Title string
Body string
Status string `gorm:"not null;default:open"`
CreatedAt time.Time
UpdatedAt time.Time
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
ProjectIdentity string `gorm:"type:char(36);index"`
CreatedBy uint `gorm:"index;not null"`
CreatedByIdentity string `gorm:"type:char(36);index"`
SourceType string `gorm:"not null"`
Title string
Body string
Status string `gorm:"not null;default:open"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (SenlinAgentInboxItem) TableName() string {

View File

@@ -3,14 +3,18 @@ package models
import "time"
type SenlinAgentNote struct {
ID uint `gorm:"primaryKey"`
ProjectID uint `gorm:"index;not null"`
CreatedBy uint `gorm:"index;not null"`
SourceInboxItemID *uint `gorm:"index"`
Title string `gorm:"not null"`
Markdown string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
ProjectIdentity string `gorm:"type:char(36);index"`
CreatedBy uint `gorm:"index;not null"`
CreatedByIdentity string `gorm:"type:char(36);index"`
SourceInboxItemID *uint `gorm:"index"`
SourceInboxItemIdentity *string `gorm:"type:char(36);index"`
Title string `gorm:"not null"`
Markdown string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (SenlinAgentNote) TableName() string {

View File

@@ -3,12 +3,14 @@ package models
import "time"
type SenlinAgentProject struct {
ID uint `gorm:"primaryKey"`
OwnerID uint `gorm:"index;not null"`
Name string `gorm:"not null"`
Description string
CreatedAt time.Time
UpdatedAt time.Time
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
OwnerID uint `gorm:"index;not null"`
OwnerIdentity string `gorm:"type:char(36);index"`
Name string `gorm:"not null"`
Description string
CreatedAt time.Time
UpdatedAt time.Time
}
func (SenlinAgentProject) TableName() string {

View File

@@ -3,14 +3,18 @@ package models
import "time"
type SenlinAgentProjectEvent struct {
ID uint `gorm:"primaryKey"`
ProjectID uint `gorm:"index;not null"`
ActorID uint `gorm:"index;not null"`
EventType string `gorm:"not null"`
EntityType string `gorm:"not null"`
EntityID uint `gorm:"not null"`
Summary string `gorm:"not null"`
CreatedAt time.Time
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
ProjectIdentity string `gorm:"type:char(36);index"`
ActorID uint `gorm:"index;not null"`
ActorIdentity string `gorm:"type:char(36);index"`
EventType string `gorm:"not null"`
EntityType string `gorm:"not null"`
EntityID uint `gorm:"not null;index"`
EntityIdentity string `gorm:"type:char(36);index"`
Summary string `gorm:"not null"`
CreatedAt time.Time
}
func (SenlinAgentProjectEvent) TableName() string {

View File

@@ -3,17 +3,21 @@ package models
import "time"
type SenlinAgentSource struct {
ID uint `gorm:"primaryKey"`
ProjectID uint `gorm:"index;not null"`
CreatedBy uint `gorm:"index;not null"`
SourceInboxItemID *uint `gorm:"index"`
Kind string `gorm:"not null"`
Title string `gorm:"not null"`
URL string
FilePath string
ContentText string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
ProjectIdentity string `gorm:"type:char(36);index"`
CreatedBy uint `gorm:"index;not null"`
CreatedByIdentity string `gorm:"type:char(36);index"`
SourceInboxItemID *uint `gorm:"index"`
SourceInboxItemIdentity *string `gorm:"type:char(36);index"`
Kind string `gorm:"not null"`
Title string `gorm:"not null"`
URL string
FilePath string
ContentText string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (SenlinAgentSource) TableName() string {

View File

@@ -3,10 +3,12 @@ package models
import "time"
type SenlinAgentTag struct {
ID uint `gorm:"primaryKey"`
ProjectID uint `gorm:"index;not null"`
Name string `gorm:"not null"`
CreatedAt time.Time
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
ProjectIdentity string `gorm:"type:char(36);index"`
Name string `gorm:"not null"`
CreatedAt time.Time
}
func (SenlinAgentTag) TableName() string {

View File

@@ -3,18 +3,23 @@ package models
import "time"
type SenlinAgentTask struct {
ID uint `gorm:"primaryKey"`
ProjectID uint `gorm:"index;not null"`
CreatedBy uint `gorm:"index;not null"`
AssigneeID *uint `gorm:"index"`
SourceInboxItemID *uint `gorm:"index"`
Title string `gorm:"not null"`
Description string
Status string `gorm:"not null;default:open"`
SortOrder int `gorm:"not null;default:0"`
DueAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
ProjectID uint `gorm:"index;not null"`
ProjectIdentity string `gorm:"type:char(36);index"`
CreatedBy uint `gorm:"index;not null"`
CreatedByIdentity string `gorm:"type:char(36);index"`
AssigneeID *uint `gorm:"index"`
AssigneeIdentity *string `gorm:"type:char(36);index"`
SourceInboxItemID *uint `gorm:"index"`
SourceInboxItemIdentity *string `gorm:"type:char(36);index"`
Title string `gorm:"not null"`
Description string
Status string `gorm:"not null;default:open"`
SortOrder int `gorm:"not null;default:0"`
DueAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
func (SenlinAgentTask) TableName() string {

View File

@@ -3,11 +3,14 @@ package models
import "time"
type SenlinAgentTaskShare struct {
ID uint `gorm:"primaryKey"`
TaskID uint `gorm:"index;not null"`
ObjectType string `gorm:"not null"`
ObjectID uint `gorm:"not null"`
CreatedAt time.Time
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
TaskID uint `gorm:"index;not null"`
TaskIdentity string `gorm:"type:char(36);index"`
ObjectType string `gorm:"not null"`
ObjectID uint `gorm:"not null;index"`
ObjectIdentity string `gorm:"type:char(36);index"`
CreatedAt time.Time
}
func (SenlinAgentTaskShare) TableName() string {

View File

@@ -4,6 +4,7 @@ import "time"
type SenlinAgentUser struct {
ID uint `gorm:"primaryKey"`
Identity string `gorm:"type:char(36);uniqueIndex"`
Email string `gorm:"uniqueIndex;not null"`
DisplayName string `gorm:"not null"`
PasswordHash string `gorm:"not null"`