reseed feature adjustment
This commit is contained in:
parent
d4bf2e8642
commit
36134f32a2
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
@ -16,6 +16,16 @@ type resetAndReseedReq struct {
|
||||||
Confirm string `json:"confirm"`
|
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) {
|
func resolveSeedDir(seedDir string) (string, error) {
|
||||||
cleanSeedDir := strings.TrimSpace(seedDir)
|
cleanSeedDir := strings.TrimSpace(seedDir)
|
||||||
if cleanSeedDir == "" {
|
if cleanSeedDir == "" {
|
||||||
|
|
@ -64,7 +74,7 @@ func resolveSeedDir(seedDir string) (string, error) {
|
||||||
|
|
||||||
// ResetAndReseedDatabase godoc
|
// ResetAndReseedDatabase godoc
|
||||||
// @Summary Reset and reseed database
|
// @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
|
// @Tags internal
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
|
@ -115,92 +125,50 @@ func (h *Handler) ResetAndReseedDatabase(c *fiber.Ctx) error {
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
seedFiles, err := filepath.Glob(filepath.Join(seedDir, "*.sql"))
|
seedCandidates := []string{
|
||||||
if err != nil {
|
filepath.Join(seedDir, "007_course_management_seed.sql"),
|
||||||
|
filepath.Join(seedDir, "001_initial_seed_data.sql"),
|
||||||
|
}
|
||||||
|
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{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to resolve seed files",
|
Message: "Missing required seed statement",
|
||||||
Error: err.Error(),
|
Error: fmt.Sprintf("could not find INSERT INTO %s in seed files", tableName),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
sort.Strings(seedFiles)
|
|
||||||
|
|
||||||
var sqlBuilder strings.Builder
|
var sqlBuilder strings.Builder
|
||||||
sqlBuilder.WriteString("BEGIN;\n")
|
sqlBuilder.WriteString("BEGIN;\n")
|
||||||
// Ensure we never attempt to insert NULL set_id into question_set_items.
|
sqlBuilder.WriteString("TRUNCATE TABLE sub_courses, courses, course_categories RESTART IDENTITY CASCADE;\n")
|
||||||
// Some deployments may already have the buggy function from migration 000024,
|
for _, tableName := range tableNames {
|
||||||
// so we patch it at runtime to make reseed safe.
|
sqlBuilder.WriteString("\n-- ")
|
||||||
sqlBuilder.WriteString(`
|
sqlBuilder.WriteString(tableName)
|
||||||
CREATE OR REPLACE FUNCTION clone_default_initial_assessment_items(target_set_id BIGINT)
|
sqlBuilder.WriteString(" from ")
|
||||||
RETURNS VOID AS $$
|
sqlBuilder.WriteString(statementSource[tableName])
|
||||||
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("\n")
|
sqlBuilder.WriteString("\n")
|
||||||
sqlBuilder.Write(content)
|
sqlBuilder.WriteString(statements[tableName])
|
||||||
sqlBuilder.WriteString("\n")
|
sqlBuilder.WriteString("\n")
|
||||||
}
|
}
|
||||||
sqlBuilder.WriteString("COMMIT;")
|
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{
|
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{}{
|
Data: map[string]interface{}{
|
||||||
"seed_dir": seedDir,
|
"seed_dir": seedDir,
|
||||||
"seed_files": seedFiles,
|
"tables": tableNames,
|
||||||
"seeded_count": len(seedFiles),
|
"sources": statementSource,
|
||||||
},
|
},
|
||||||
Success: true,
|
Success: true,
|
||||||
StatusCode: fiber.StatusOK,
|
StatusCode: fiber.StatusOK,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user