0e8de6192c
Wires KnowledgeBase.VectorStoreID and the ownership-aware retrieve factory into the user-facing knowledge-base lifecycle: - POST /knowledge-bases validates the requested vector_store_id against the caller's tenant scope and the engine registry. New error codes ErrVectorStoreBindingInvalid (2200) and ErrVectorStoreUnavailable (2201) distinguish the typed branches without echoing UUIDs to the client. - GET / POST / PUT / PUT-pin responses embed the bound store's display metadata (name, source, engine_type, status) without exposing any connection credentials. Cross-tenant shared KBs receive a suppressed payload (vector_store_id stripped, source="shared") so operator-chosen store names cannot be enumerated across tenants. - POST /knowledge-bases/copy synchronously rejects clones whose target has a different embedding model or vector store, before the async clone task is enqueued. The async clone worker re-applies the same checks for defense in depth. - DELETE /vector-stores/:id refuses to remove a store with bound KBs, inside a transaction that row-locks the store on PostgreSQL and serializes via WAL on SQLite. unregister-from-registry is wrapped in defer/recover so a panic surfaces as a structured warning instead of silently leaking a stale engine. - vector_store_id is immutable after creation. The GORM <-:create tag blocks every ORM update path; the service-layer DTO omits the field entirely; a reflection-based regression test catches any future maintainer who adds it back to either layer. - Empty-string vector_store_id is normalized to nil at both the create path and inside SharesStoreWith, so rows persisted by callers that did not run Normalize first cannot trip false same-store comparisons. Part of #993. Depends on #994 and #1310.
151 lines
5.1 KiB
Go
151 lines
5.1 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var ErrKnowledgeBaseNotFound = errors.New("knowledge base not found")
|
|
|
|
// knowledgeBaseRepository implements the KnowledgeBaseRepository interface
|
|
type knowledgeBaseRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewKnowledgeBaseRepository creates a new knowledge base repository
|
|
func NewKnowledgeBaseRepository(db *gorm.DB) interfaces.KnowledgeBaseRepository {
|
|
return &knowledgeBaseRepository{db: db}
|
|
}
|
|
|
|
// CreateKnowledgeBase creates a new knowledge base
|
|
func (r *knowledgeBaseRepository) CreateKnowledgeBase(ctx context.Context, kb *types.KnowledgeBase) error {
|
|
return r.db.WithContext(ctx).Create(kb).Error
|
|
}
|
|
|
|
// GetKnowledgeBaseByID gets a knowledge base by id (no tenant scope; caller must enforce isolation where needed)
|
|
func (r *knowledgeBaseRepository) GetKnowledgeBaseByID(ctx context.Context, id string) (*types.KnowledgeBase, error) {
|
|
var kb types.KnowledgeBase
|
|
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&kb).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, ErrKnowledgeBaseNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return &kb, nil
|
|
}
|
|
|
|
// GetKnowledgeBaseByIDAndTenant gets a knowledge base by id only if it belongs to the given tenant (enforces tenant isolation)
|
|
func (r *knowledgeBaseRepository) GetKnowledgeBaseByIDAndTenant(ctx context.Context, id string, tenantID uint64) (*types.KnowledgeBase, error) {
|
|
var kb types.KnowledgeBase
|
|
if err := r.db.WithContext(ctx).Where("id = ? AND tenant_id = ?", id, tenantID).First(&kb).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, ErrKnowledgeBaseNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return &kb, nil
|
|
}
|
|
|
|
// GetKnowledgeBaseByIDs gets knowledge bases by multiple ids
|
|
func (r *knowledgeBaseRepository) GetKnowledgeBaseByIDs(ctx context.Context, ids []string) ([]*types.KnowledgeBase, error) {
|
|
if len(ids) == 0 {
|
|
return []*types.KnowledgeBase{}, nil
|
|
}
|
|
var kbs []*types.KnowledgeBase
|
|
if err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&kbs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return kbs, nil
|
|
}
|
|
|
|
// ListKnowledgeBases lists all knowledge bases
|
|
func (r *knowledgeBaseRepository) ListKnowledgeBases(ctx context.Context) ([]*types.KnowledgeBase, error) {
|
|
var kbs []*types.KnowledgeBase
|
|
if err := r.db.WithContext(ctx).Find(&kbs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return kbs, nil
|
|
}
|
|
|
|
// ListKnowledgeBasesByTenantID lists all knowledge bases by tenant id
|
|
func (r *knowledgeBaseRepository) ListKnowledgeBasesByTenantID(
|
|
ctx context.Context, tenantID uint64,
|
|
) ([]*types.KnowledgeBase, error) {
|
|
var kbs []*types.KnowledgeBase
|
|
if err := r.db.WithContext(ctx).Where("tenant_id = ? AND is_temporary = ?", tenantID, false).
|
|
Order("is_pinned DESC, pinned_at DESC, created_at DESC").Find(&kbs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return kbs, nil
|
|
}
|
|
|
|
// TogglePinKnowledgeBase toggles the pin status of a knowledge base
|
|
func (r *knowledgeBaseRepository) TogglePinKnowledgeBase(ctx context.Context, id string, tenantID uint64) (*types.KnowledgeBase, error) {
|
|
var kb types.KnowledgeBase
|
|
if err := r.db.WithContext(ctx).Where("id = ? AND tenant_id = ?", id, tenantID).First(&kb).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, ErrKnowledgeBaseNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if kb.IsPinned {
|
|
kb.IsPinned = false
|
|
kb.PinnedAt = nil
|
|
} else {
|
|
kb.IsPinned = true
|
|
now := time.Now()
|
|
kb.PinnedAt = &now
|
|
}
|
|
|
|
if err := r.db.WithContext(ctx).Model(&kb).Updates(map[string]interface{}{
|
|
"is_pinned": kb.IsPinned,
|
|
"pinned_at": kb.PinnedAt,
|
|
}).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &kb, nil
|
|
}
|
|
|
|
// UpdateKnowledgeBase updates a knowledge base
|
|
func (r *knowledgeBaseRepository) UpdateKnowledgeBase(ctx context.Context, kb *types.KnowledgeBase) error {
|
|
return r.db.WithContext(ctx).Save(kb).Error
|
|
}
|
|
|
|
// DeleteKnowledgeBase deletes a knowledge base
|
|
func (r *knowledgeBaseRepository) DeleteKnowledgeBase(ctx context.Context, id string) error {
|
|
return r.db.WithContext(ctx).Where("id = ?", id).Delete(&types.KnowledgeBase{}).Error
|
|
}
|
|
|
|
// CountByVectorStoreID counts active knowledge bases that are bound to the
|
|
// given vector store within a tenant scope.
|
|
//
|
|
// Soft-delete filter is applied automatically by GORM because KnowledgeBase
|
|
// has a gorm.DeletedAt column — we deliberately do not add an explicit
|
|
// `deleted_at IS NULL` predicate to keep the single source of truth on the
|
|
// auto-scope.
|
|
//
|
|
// Pass db == nil to use the repository's default db handle; pass a *gorm.DB
|
|
// bound to a transaction (e.g., from db.Transaction) to share the same
|
|
// write-lock context as the caller. Query column order matches the
|
|
// composite index idx_knowledge_bases_tenant_vector_store(tenant_id,
|
|
// vector_store_id).
|
|
func (r *knowledgeBaseRepository) CountByVectorStoreID(
|
|
ctx context.Context, db *gorm.DB, tenantID uint64, storeID string,
|
|
) (int64, error) {
|
|
if db == nil {
|
|
db = r.db
|
|
}
|
|
var count int64
|
|
err := db.WithContext(ctx).
|
|
Model(&types.KnowledgeBase{}).
|
|
Where("tenant_id = ? AND vector_store_id = ?", tenantID, storeID).
|
|
Count(&count).Error
|
|
return count, err
|
|
}
|