diff --git a/internal/repository/job.go b/internal/repository/job.go index 032c342..6ae612d 100644 --- a/internal/repository/job.go +++ b/internal/repository/job.go @@ -2,6 +2,63 @@ // All rights reserved. This file is part of cc-backend. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. + +// Package repository provides the data access layer for cc-backend using the repository pattern. +// +// The repository pattern abstracts database operations and provides a clean interface for +// data access. Each major entity (Job, User, Node, Tag) has its own repository with CRUD +// operations and specialized queries. +// +// # Database Connection +// +// Initialize the database connection before using any repository: +// +// repository.Connect("sqlite3", "./var/job.db") +// // or for MySQL: +// repository.Connect("mysql", "user:password@tcp(localhost:3306)/dbname") +// +// # Configuration +// +// Optional: Configure repository settings before initialization: +// +// repository.SetConfig(&repository.RepositoryConfig{ +// CacheSize: 2 * 1024 * 1024, // 2MB cache +// MaxOpenConnections: 8, // Connection pool size +// MinRunningJobDuration: 300, // Filter threshold +// }) +// +// If not configured, sensible defaults are used automatically. +// +// # Repositories +// +// - JobRepository: Job lifecycle management and querying +// - UserRepository: User management and authentication +// - NodeRepository: Cluster node state tracking +// - Tags: Job tagging and categorization +// +// # Caching +// +// Repositories use LRU caching to improve performance. Cache keys are constructed +// as "type:id" (e.g., "metadata:123"). Cache is automatically invalidated on +// mutations to maintain consistency. +// +// # Transaction Support +// +// For batch operations, use transactions: +// +// t, err := jobRepo.TransactionInit() +// if err != nil { +// return err +// } +// defer t.Rollback() // Rollback if not committed +// +// // Perform operations... +// jobRepo.TransactionAdd(t, query, args...) +// +// // Commit when done +// if err := t.Commit(); err != nil { +// return err +// } package repository import ( diff --git a/internal/repository/tags.go b/internal/repository/tags.go index 87bf69d..52bd907 100644 --- a/internal/repository/tags.go +++ b/internal/repository/tags.go @@ -5,6 +5,7 @@ package repository import ( + "errors" "fmt" "strings" @@ -14,65 +15,32 @@ import ( sq "github.com/Masterminds/squirrel" ) -// Add the tag with id `tagId` to the job with the database id `jobId`. +// AddTag adds the tag with id `tagId` to the job with the database id `jobId`. +// Requires user authentication for security checks. func (r *JobRepository) AddTag(user *schema.User, job int64, tag int64) ([]*schema.Tag, error) { j, err := r.FindByIdWithUser(user, job) if err != nil { - cclog.Warn("Error while finding job by id") + cclog.Warnf("Error finding job %d for user %s: %v", job, user.Username, err) return nil, err } - q := sq.Insert("jobtag").Columns("job_id", "tag_id").Values(job, tag) - - if _, err := q.RunWith(r.stmtCache).Exec(); err != nil { - s, _, _ := q.ToSql() - cclog.Errorf("Error adding tag with %s: %v", s, err) - return nil, err - } - - tags, err := r.GetTags(user, &job) - if err != nil { - cclog.Warn("Error while getting tags for job") - return nil, err - } - - archiveTags, err := r.getArchiveTags(&job) - if err != nil { - cclog.Warn("Error while getting tags for job") - return nil, err - } - - return tags, archive.UpdateTags(j, archiveTags) + return r.addJobTag(job, tag, j, func() ([]*schema.Tag, error) { + return r.GetTags(user, &job) + }) } +// AddTagDirect adds a tag without user security checks. +// Use only for internal/admin operations. func (r *JobRepository) AddTagDirect(job int64, tag int64) ([]*schema.Tag, error) { j, err := r.FindByIdDirect(job) if err != nil { - cclog.Warn("Error while finding job by id") + cclog.Warnf("Error finding job %d: %v", job, err) return nil, err } - q := sq.Insert("jobtag").Columns("job_id", "tag_id").Values(job, tag) - - if _, err := q.RunWith(r.stmtCache).Exec(); err != nil { - s, _, _ := q.ToSql() - cclog.Errorf("Error adding tag with %s: %v", s, err) - return nil, err - } - - tags, err := r.GetTagsDirect(&job) - if err != nil { - cclog.Warn("Error while getting tags for job") - return nil, err - } - - archiveTags, err := r.getArchiveTags(&job) - if err != nil { - cclog.Warn("Error while getting tags for job") - return nil, err - } - - return tags, archive.UpdateTags(j, archiveTags) + return r.addJobTag(job, tag, j, func() ([]*schema.Tag, error) { + return r.GetTagsDirect(&job) + }) } // Removes a tag from a job by tag id. @@ -260,15 +228,18 @@ func (r *JobRepository) CountTags(user *schema.User) (tags []schema.Tag, counts LeftJoin("jobtag jt ON t.id = jt.tag_id"). GroupBy("t.tag_name") - // Handle Scope Filtering - scopeList := "\"global\"" + // Build scope list for filtering + var scopeBuilder strings.Builder + scopeBuilder.WriteString(`"global"`) if user != nil { - scopeList += ",\"" + user.Username + "\"" + scopeBuilder.WriteString(`,"`) + scopeBuilder.WriteString(user.Username) + scopeBuilder.WriteString(`"`) + if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) { + scopeBuilder.WriteString(`,"admin"`) + } } - if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) { - scopeList += ",\"admin\"" - } - q = q.Where("t.tag_scope IN (" + scopeList + ")") + q = q.Where("t.tag_scope IN (" + scopeBuilder.String() + ")") // Handle Job Ownership if user != nil && user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) { // ADMIN || SUPPORT: Count all jobs @@ -302,6 +273,41 @@ func (r *JobRepository) CountTags(user *schema.User) (tags []schema.Tag, counts return tags, counts, err } +var ( + ErrTagNotFound = errors.New("the tag does not exist") + ErrJobNotOwned = errors.New("user is not owner of job") + ErrTagNoAccess = errors.New("user not permitted to use that tag") + ErrTagPrivateScope = errors.New("tag is private to another user") + ErrTagAdminScope = errors.New("tag requires admin privileges") + ErrTagsIncompatScopes = errors.New("combining admin and non-admin scoped tags not allowed") +) + +// addJobTag is a helper function that inserts a job-tag association and updates the archive. +// Returns the updated tag list for the job. +func (r *JobRepository) addJobTag(jobId int64, tagId int64, job *schema.Job, getTags func() ([]*schema.Tag, error)) ([]*schema.Tag, error) { + q := sq.Insert("jobtag").Columns("job_id", "tag_id").Values(jobId, tagId) + + if _, err := q.RunWith(r.stmtCache).Exec(); err != nil { + s, _, _ := q.ToSql() + cclog.Errorf("Error adding tag with %s: %v", s, err) + return nil, err + } + + tags, err := getTags() + if err != nil { + cclog.Warnf("Error getting tags for job %d: %v", jobId, err) + return nil, err + } + + archiveTags, err := r.getArchiveTags(&jobId) + if err != nil { + cclog.Warnf("Error getting archive tags for job %d: %v", jobId, err) + return nil, err + } + + return tags, archive.UpdateTags(job, archiveTags) +} + // AddTagOrCreate adds the tag with the specified type and name to the job with the database id `jobId`. // If such a tag does not yet exist, it is created. func (r *JobRepository) AddTagOrCreate(user *schema.User, jobId int64, tagType string, tagName string, tagScope string) (tagId int64, err error) {