add model identities and restore httpx router
This commit is contained in:
@@ -4,11 +4,11 @@ import (
|
||||
"log"
|
||||
|
||||
"senlinai-agent/backend/internal/config"
|
||||
"senlinai-agent/backend/internal/httpx"
|
||||
"senlinai-agent/backend/internal/logic/auth"
|
||||
"senlinai-agent/backend/internal/logic/inbox"
|
||||
"senlinai-agent/backend/internal/logic/projects"
|
||||
"senlinai-agent/backend/internal/models"
|
||||
"senlinai-agent/backend/internal/router"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -22,7 +22,7 @@ func main() {
|
||||
authService := auth.NewService(cfg.AuthSecret)
|
||||
projectHandler := projects.NewHandler(projects.NewService())
|
||||
inboxHandler := inbox.NewHandler(inbox.NewService(inbox.StaticAnalyzer{}))
|
||||
appRouter := router.NewProtectedRouter(cfg, authService.VerifySession, projectHandler, inboxHandler)
|
||||
appRouter := httpx.NewProtectedRouter(cfg, authService.VerifySession, projectHandler, inboxHandler)
|
||||
if err := appRouter.Run(":" + cfg.Port); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
|
||||
@@ -40,6 +40,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package router
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -27,6 +27,7 @@ func newRouter(cfg config.Config, tokenVerifier func(string) (uint, error), regi
|
||||
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
router.Use(cors())
|
||||
|
||||
router.GET("/healthz", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
@@ -41,3 +42,29 @@ func newRouter(cfg config.Config, tokenVerifier func(string) (uint, error), regi
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func cors() gin.HandlerFunc {
|
||||
allowedOrigins := map[string]bool{
|
||||
"http://localhost:5173": true,
|
||||
"http://127.0.0.1:5173": true,
|
||||
"http://tauri.localhost": true,
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
origin := c.GetHeader("Origin")
|
||||
if allowedOrigins[origin] {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
c.Header("Vary", "Origin")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type")
|
||||
c.Header("Access-Control-Max-Age", "86400")
|
||||
}
|
||||
|
||||
if c.Request.Method == http.MethodOptions {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
71
backend/internal/httpx/router_test.go
Normal file
71
backend/internal/httpx/router_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"senlinai-agent/backend/internal/config"
|
||||
)
|
||||
|
||||
func TestHealthz(t *testing.T) {
|
||||
router := NewRouter(config.Config{Env: "test"})
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.JSONEq(t, `{"status":"ok"}`, rec.Body.String())
|
||||
}
|
||||
|
||||
func TestNewRouterRegistersFeatureRoutesUnderAPI(t *testing.T) {
|
||||
router := NewRouter(config.Config{Env: "test"}, testRegistrar{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/ping", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.JSONEq(t, `{"pong":true}`, rec.Body.String())
|
||||
}
|
||||
|
||||
func TestRouterAddsCORSHeadersForLocalWebClient(t *testing.T) {
|
||||
router := NewRouter(config.Config{Env: "test"}, testRegistrar{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/ping", nil)
|
||||
req.Header.Set("Origin", "http://localhost:5173")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, "http://localhost:5173", rec.Header().Get("Access-Control-Allow-Origin"))
|
||||
require.Contains(t, rec.Header().Get("Access-Control-Allow-Headers"), "Authorization")
|
||||
}
|
||||
|
||||
func TestRouterHandlesCORSPreflightBeforeAuth(t *testing.T) {
|
||||
router := NewProtectedRouter(config.Config{Env: "test"}, func(token string) (uint, error) {
|
||||
return 0, http.ErrNoCookie
|
||||
}, testRegistrar{})
|
||||
req := httptest.NewRequest(http.MethodOptions, "/api/ping", nil)
|
||||
req.Header.Set("Origin", "http://localhost:5173")
|
||||
req.Header.Set("Access-Control-Request-Method", "GET")
|
||||
req.Header.Set("Access-Control-Request-Headers", "Authorization")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusNoContent, rec.Code)
|
||||
require.Equal(t, "http://localhost:5173", rec.Header().Get("Access-Control-Allow-Origin"))
|
||||
require.Contains(t, rec.Header().Get("Access-Control-Allow-Headers"), "Authorization")
|
||||
}
|
||||
|
||||
type testRegistrar struct{}
|
||||
|
||||
func (testRegistrar) Register(router gin.IRouter) {
|
||||
router.GET("/ping", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"pong": true})
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
202
backend/internal/models/identity.go
Normal file
202
backend/internal/models/identity.go
Normal 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
|
||||
}
|
||||
49
backend/internal/models/identity_test.go
Normal file
49
backend/internal/models/identity_test.go
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"senlinai-agent/backend/internal/config"
|
||||
)
|
||||
|
||||
func TestHealthz(t *testing.T) {
|
||||
router := NewRouter(config.Config{Env: "test"})
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.JSONEq(t, `{"status":"ok"}`, rec.Body.String())
|
||||
}
|
||||
|
||||
func TestNewRouterRegistersFeatureRoutesUnderAPI(t *testing.T) {
|
||||
router := NewRouter(config.Config{Env: "test"}, testRegistrar{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/ping", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.JSONEq(t, `{"pong":true}`, rec.Body.String())
|
||||
}
|
||||
|
||||
type testRegistrar struct{}
|
||||
|
||||
func (testRegistrar) Register(router gin.IRouter) {
|
||||
router.GET("/ping", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"pong": true})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user