Files
Enterprise-Knowledge-Base/internal/application/repository/organization.go
T
wizardchen cbe66aba56 feat(api): integrate per-organization resource counts into organization listing
- Added `resource_counts` to the `ListOrganizationsResponse` to include per-organization knowledge base and agent counts, enhancing the sidebar display.
- Removed the deprecated `getMeResourceCounts` API endpoint, consolidating resource count retrieval into the organization listing.
- Updated the organization store to manage and utilize the new resource counts effectively.
- Adjusted UI components to leverage the new resource counts structure, improving data retrieval and display for agents and knowledge bases.

This update streamlines resource count management and enhances the user experience by providing relevant data in a single API call.
2026-02-09 11:40:25 +08:00

326 lines
11 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 (
ErrOrganizationNotFound = errors.New("organization not found")
ErrOrgMemberNotFound = errors.New("organization member not found")
ErrOrgMemberAlreadyExists = errors.New("member already exists in organization")
ErrInviteCodeNotFound = errors.New("invite code not found")
ErrInviteCodeExpired = errors.New("invite code has expired")
)
// organizationRepository implements OrganizationRepository interface
type organizationRepository struct {
db *gorm.DB
}
// NewOrganizationRepository creates a new organization repository
func NewOrganizationRepository(db *gorm.DB) interfaces.OrganizationRepository {
return &organizationRepository{db: db}
}
// Create creates a new organization
func (r *organizationRepository) Create(ctx context.Context, org *types.Organization) error {
return r.db.WithContext(ctx).Create(org).Error
}
// GetByID gets an organization by ID
func (r *organizationRepository) GetByID(ctx context.Context, id string) (*types.Organization, error) {
var org types.Organization
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&org).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrOrganizationNotFound
}
return nil, err
}
return &org, nil
}
// GetByInviteCode gets an organization by invite code (returns ErrInviteCodeExpired if code has expired)
func (r *organizationRepository) GetByInviteCode(ctx context.Context, inviteCode string) (*types.Organization, error) {
var org types.Organization
if err := r.db.WithContext(ctx).Where("invite_code = ?", inviteCode).First(&org).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrInviteCodeNotFound
}
return nil, err
}
if org.InviteCodeExpiresAt != nil && org.InviteCodeExpiresAt.Before(time.Now()) {
return nil, ErrInviteCodeExpired
}
return &org, nil
}
// ListByUserID lists organizations that a user belongs to
func (r *organizationRepository) ListByUserID(ctx context.Context, userID string) ([]*types.Organization, error) {
var orgs []*types.Organization
// Get organizations where user is a member
err := r.db.WithContext(ctx).
Joins("JOIN organization_members ON organization_members.organization_id = organizations.id").
Where("organization_members.user_id = ?", userID).
Order("organizations.created_at DESC").
Find(&orgs).Error
if err != nil {
return nil, err
}
return orgs, nil
}
// ListSearchable lists organizations that are searchable (open for discovery), optionally filtered by name/description/ID
func (r *organizationRepository) ListSearchable(ctx context.Context, query string, limit int) ([]*types.Organization, error) {
if limit <= 0 {
limit = 20
}
var orgs []*types.Organization
q := r.db.WithContext(ctx).Where("searchable = ?", true)
if query != "" {
pattern := "%" + query + "%"
// 支持按名称、描述或空间 ID 搜索,便于区分同名空间
q = q.Where("name ILIKE ? OR description ILIKE ? OR id::text ILIKE ?", pattern, pattern, pattern)
}
err := q.Order("created_at DESC").Limit(limit).Find(&orgs).Error
if err != nil {
return nil, err
}
return orgs, nil
}
// Update updates an organization (Select ensures zero values like invite_code_validity_days=0 are persisted)
func (r *organizationRepository) Update(ctx context.Context, org *types.Organization) error {
return r.db.WithContext(ctx).Model(&types.Organization{}).Where("id = ?", org.ID).
Select("name", "description", "avatar", "require_approval", "searchable", "invite_code_validity_days", "member_limit", "updated_at").
Updates(org).Error
}
// Delete soft deletes an organization
func (r *organizationRepository) Delete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Where("id = ?", id).Delete(&types.Organization{}).Error
}
// AddMember adds a member to an organization
func (r *organizationRepository) AddMember(ctx context.Context, member *types.OrganizationMember) error {
// Check if member already exists
var count int64
r.db.WithContext(ctx).Model(&types.OrganizationMember{}).
Where("organization_id = ? AND user_id = ?", member.OrganizationID, member.UserID).
Count(&count)
if count > 0 {
return ErrOrgMemberAlreadyExists
}
return r.db.WithContext(ctx).Create(member).Error
}
// RemoveMember removes a member from an organization
func (r *organizationRepository) RemoveMember(ctx context.Context, orgID string, userID string) error {
result := r.db.WithContext(ctx).
Where("organization_id = ? AND user_id = ?", orgID, userID).
Delete(&types.OrganizationMember{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrOrgMemberNotFound
}
return nil
}
// UpdateMemberRole updates a member's role in an organization
func (r *organizationRepository) UpdateMemberRole(ctx context.Context, orgID string, userID string, role types.OrgMemberRole) error {
result := r.db.WithContext(ctx).
Model(&types.OrganizationMember{}).
Where("organization_id = ? AND user_id = ?", orgID, userID).
Update("role", role)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrOrgMemberNotFound
}
return nil
}
// ListMembers lists all members of an organization
func (r *organizationRepository) ListMembers(ctx context.Context, orgID string) ([]*types.OrganizationMember, error) {
var members []*types.OrganizationMember
err := r.db.WithContext(ctx).
Preload("User").
Where("organization_id = ?", orgID).
Order("created_at ASC").
Find(&members).Error
if err != nil {
return nil, err
}
return members, nil
}
// GetMember gets a specific member of an organization
func (r *organizationRepository) GetMember(ctx context.Context, orgID string, userID string) (*types.OrganizationMember, error) {
var member types.OrganizationMember
err := r.db.WithContext(ctx).
Where("organization_id = ? AND user_id = ?", orgID, userID).
First(&member).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrOrgMemberNotFound
}
return nil, err
}
return &member, nil
}
// ListMembersByUserForOrgs returns one member record per org where the user is a member (batch).
func (r *organizationRepository) ListMembersByUserForOrgs(ctx context.Context, userID string, orgIDs []string) (map[string]*types.OrganizationMember, error) {
if len(orgIDs) == 0 {
return make(map[string]*types.OrganizationMember), nil
}
var members []*types.OrganizationMember
err := r.db.WithContext(ctx).
Where("user_id = ? AND organization_id IN ?", userID, orgIDs).
Find(&members).Error
if err != nil {
return nil, err
}
out := make(map[string]*types.OrganizationMember, len(members))
for _, m := range members {
if m != nil {
out[m.OrganizationID] = m
}
}
return out, nil
}
// CountMembers counts the number of members in an organization
func (r *organizationRepository) CountMembers(ctx context.Context, orgID string) (int64, error) {
var count int64
err := r.db.WithContext(ctx).
Model(&types.OrganizationMember{}).
Where("organization_id = ?", orgID).
Count(&count).Error
return count, err
}
// UpdateInviteCode updates the invite code and optional expiry for an organization (expiresAt nil = never expire)
func (r *organizationRepository) UpdateInviteCode(ctx context.Context, orgID string, inviteCode string, expiresAt *time.Time) error {
updates := map[string]interface{}{"invite_code": inviteCode, "invite_code_expires_at": expiresAt}
return r.db.WithContext(ctx).
Model(&types.Organization{}).
Where("id = ?", orgID).
Updates(updates).Error
}
// ----------------
// Join Requests
// ----------------
var ErrJoinRequestNotFound = errors.New("join request not found")
// CreateJoinRequest creates a new join request
func (r *organizationRepository) CreateJoinRequest(ctx context.Context, request *types.OrganizationJoinRequest) error {
return r.db.WithContext(ctx).Create(request).Error
}
// GetJoinRequestByID gets a join request by ID
func (r *organizationRepository) GetJoinRequestByID(ctx context.Context, id string) (*types.OrganizationJoinRequest, error) {
var request types.OrganizationJoinRequest
err := r.db.WithContext(ctx).
Preload("User").
Where("id = ?", id).
First(&request).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrJoinRequestNotFound
}
return nil, err
}
return &request, nil
}
// GetPendingJoinRequest gets a pending join request for a user in an organization (any type)
func (r *organizationRepository) GetPendingJoinRequest(ctx context.Context, orgID string, userID string) (*types.OrganizationJoinRequest, error) {
var request types.OrganizationJoinRequest
err := r.db.WithContext(ctx).
Where("organization_id = ? AND user_id = ? AND status = ?", orgID, userID, types.JoinRequestStatusPending).
First(&request).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrJoinRequestNotFound
}
return nil, err
}
return &request, nil
}
// GetPendingRequestByType gets a pending request for a user filtered by request type
func (r *organizationRepository) GetPendingRequestByType(ctx context.Context, orgID string, userID string, requestType types.JoinRequestType) (*types.OrganizationJoinRequest, error) {
var request types.OrganizationJoinRequest
err := r.db.WithContext(ctx).
Where("organization_id = ? AND user_id = ? AND status = ? AND request_type = ?", orgID, userID, types.JoinRequestStatusPending, requestType).
First(&request).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrJoinRequestNotFound
}
return nil, err
}
return &request, nil
}
// ListJoinRequests lists join requests for an organization
func (r *organizationRepository) ListJoinRequests(ctx context.Context, orgID string, status types.JoinRequestStatus) ([]*types.OrganizationJoinRequest, error) {
var requests []*types.OrganizationJoinRequest
query := r.db.WithContext(ctx).
Preload("User").
Where("organization_id = ?", orgID)
if status != "" {
query = query.Where("status = ?", status)
}
err := query.Order("created_at DESC").Find(&requests).Error
if err != nil {
return nil, err
}
return requests, nil
}
// CountJoinRequests counts join requests for an organization by status
func (r *organizationRepository) CountJoinRequests(ctx context.Context, orgID string, status types.JoinRequestStatus) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&types.OrganizationJoinRequest{}).Where("organization_id = ?", orgID)
if status != "" {
query = query.Where("status = ?", status)
}
err := query.Count(&count).Error
return count, err
}
// UpdateJoinRequestStatus updates the status of a join request
func (r *organizationRepository) UpdateJoinRequestStatus(ctx context.Context, id string, status types.JoinRequestStatus, reviewedBy string, reviewMessage string) error {
return r.db.WithContext(ctx).
Model(&types.OrganizationJoinRequest{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"status": status,
"reviewed_by": reviewedBy,
"reviewed_at": gorm.Expr("NOW()"),
"review_message": reviewMessage,
}).Error
}