From 36134f32a288973cf1e7540d48ab701f23513eaf Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Fri, 27 Mar 2026 03:29:33 -0700 Subject: [PATCH] reseed feature adjustment --- .../handlers/maintenance_handler.go | 160 ++++++------------ 1 file changed, 54 insertions(+), 106 deletions(-) diff --git a/internal/web_server/handlers/maintenance_handler.go b/internal/web_server/handlers/maintenance_handler.go index 6e811c3..6660781 100644 --- a/internal/web_server/handlers/maintenance_handler.go +++ b/internal/web_server/handlers/maintenance_handler.go @@ -6,7 +6,7 @@ import ( "fmt" "os" "path/filepath" - "sort" + "regexp" "strings" "github.com/gofiber/fiber/v2" @@ -16,6 +16,16 @@ type resetAndReseedReq struct { Confirm string `json:"confirm"` } +func extractInsertStatement(sqlContent string, tableName string) (string, bool) { + pattern := fmt.Sprintf(`(?is)INSERT\s+INTO\s+%s\b.*?;`, regexp.QuoteMeta(tableName)) + re := regexp.MustCompile(pattern) + statement := strings.TrimSpace(re.FindString(sqlContent)) + if statement == "" { + return "", false + } + return statement, true +} + func resolveSeedDir(seedDir string) (string, error) { cleanSeedDir := strings.TrimSpace(seedDir) if cleanSeedDir == "" { @@ -64,7 +74,7 @@ func resolveSeedDir(seedDir string) (string, error) { // ResetAndReseedDatabase godoc // @Summary Reset and reseed database -// @Description Dangerous operation: truncates all public tables (except schema_migrations) and reseeds from db/data SQL files. +// @Description Dangerous operation: clears and reseeds only course_categories, courses, and sub_courses from seed SQL files. // @Tags internal // @Accept json // @Produce json @@ -115,92 +125,50 @@ func (h *Handler) ResetAndReseedDatabase(c *fiber.Ctx) error { Error: err.Error(), }) } - seedFiles, err := filepath.Glob(filepath.Join(seedDir, "*.sql")) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Failed to resolve seed files", - Error: err.Error(), - }) + seedCandidates := []string{ + filepath.Join(seedDir, "007_course_management_seed.sql"), + filepath.Join(seedDir, "001_initial_seed_data.sql"), } - if len(seedFiles) == 0 { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "No seed files found", - Error: fmt.Sprintf("no .sql files found in %s", seedDir), - }) + tableNames := []string{"course_categories", "courses", "sub_courses"} + statements := map[string]string{} + statementSource := map[string]string{} + + for _, file := range seedCandidates { + contentBytes, readErr := os.ReadFile(file) + if readErr != nil { + continue + } + content := string(contentBytes) + for _, tableName := range tableNames { + if _, exists := statements[tableName]; exists { + continue + } + if stmt, ok := extractInsertStatement(content, tableName); ok { + statements[tableName] = stmt + statementSource[tableName] = file + } + } + } + + for _, tableName := range tableNames { + if _, ok := statements[tableName]; !ok { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Missing required seed statement", + Error: fmt.Sprintf("could not find INSERT INTO %s in seed files", tableName), + }) + } } - sort.Strings(seedFiles) var sqlBuilder strings.Builder sqlBuilder.WriteString("BEGIN;\n") - // Ensure we never attempt to insert NULL set_id into question_set_items. - // Some deployments may already have the buggy function from migration 000024, - // so we patch it at runtime to make reseed safe. - sqlBuilder.WriteString(` -CREATE OR REPLACE FUNCTION clone_default_initial_assessment_items(target_set_id BIGINT) -RETURNS VOID AS $$ -DECLARE - template_set_id BIGINT; -BEGIN - IF target_set_id IS NULL THEN - RETURN; - END IF; - - SELECT id - INTO template_set_id - FROM question_sets - WHERE set_type = 'INITIAL_ASSESSMENT' - AND owner_type = 'STANDALONE' - AND status = 'PUBLISHED' - ORDER BY created_at DESC - LIMIT 1; - - IF template_set_id IS NULL THEN - RETURN; - END IF; - - INSERT INTO question_set_items (set_id, question_id, display_order) - SELECT target_set_id, qsi.question_id, qsi.display_order - FROM question_set_items qsi - WHERE qsi.set_id = template_set_id - AND NOT EXISTS ( - SELECT 1 - FROM question_set_items existing - WHERE existing.set_id = target_set_id - AND existing.question_id = qsi.question_id - ); -END; -$$ LANGUAGE plpgsql; -`) - sqlBuilder.WriteString(` -DO $$ -DECLARE - truncate_stmt text; -BEGIN - SELECT 'TRUNCATE TABLE ' || string_agg(format('%I.%I', schemaname, tablename), ', ') - || ' RESTART IDENTITY CASCADE' - INTO truncate_stmt - FROM pg_tables - WHERE schemaname = 'public' - AND tablename <> 'schema_migrations'; - - IF truncate_stmt IS NOT NULL THEN - EXECUTE truncate_stmt; - END IF; -END $$; -`) - - for _, file := range seedFiles { - content, readErr := os.ReadFile(file) - if readErr != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Failed to read seed file", - Error: fmt.Sprintf("%s: %v", file, readErr), - }) - } - sqlBuilder.WriteString("\n-- seed file: ") - sqlBuilder.WriteString(file) + sqlBuilder.WriteString("TRUNCATE TABLE sub_courses, courses, course_categories RESTART IDENTITY CASCADE;\n") + for _, tableName := range tableNames { + sqlBuilder.WriteString("\n-- ") + sqlBuilder.WriteString(tableName) + sqlBuilder.WriteString(" from ") + sqlBuilder.WriteString(statementSource[tableName]) sqlBuilder.WriteString("\n") - sqlBuilder.Write(content) + sqlBuilder.WriteString(statements[tableName]) sqlBuilder.WriteString("\n") } sqlBuilder.WriteString("COMMIT;") @@ -212,32 +180,12 @@ END $$; }) } - // Ensure permission definitions and default assignments are in sync after reseed. - if err := h.rbacSvc.SeedPermissions(c.Context()); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Database reseeded but RBAC permission sync failed", - Error: err.Error(), - }) - } - if err := h.rbacSvc.SeedDefaultRolePermissions(c.Context()); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Database reseeded but RBAC role default sync failed", - Error: err.Error(), - }) - } - if err := h.rbacSvc.Reload(c.Context()); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Database reseeded but RBAC cache reload failed", - Error: err.Error(), - }) - } - return c.JSON(domain.Response{ - Message: "Database reset and reseed completed successfully", + Message: "Course management hierarchy reset and reseed completed successfully", Data: map[string]interface{}{ - "seed_dir": seedDir, - "seed_files": seedFiles, - "seeded_count": len(seedFiles), + "seed_dir": seedDir, + "tables": tableNames, + "sources": statementSource, }, Success: true, StatusCode: fiber.StatusOK,