diff --git a/backend/doc/EXPORT_IMPORT.md b/backend/doc/EXPORT_IMPORT.md new file mode 100644 index 0000000..3b81f3b --- /dev/null +++ b/backend/doc/EXPORT_IMPORT.md @@ -0,0 +1,216 @@ +# Master Data Export/Import + +## Overview + +The export/import mechanism allows exporting all master data from the `model_gsmaster` and `model_learning_costs` modules to JSON files and importing them back. The exported data is ID-independent, using natural keys (name + game_system) to identify records, making it suitable for: + +- Migrating data between environments +- Version controlling master data +- Manually editing game data +- Sharing/distributing game systems + +## Supported Entities + +### From `model_learning_costs.go`: +- **Sources** (`learning_sources`) - Game books and source materials +- **SkillCategories** (`learning_skill_categories`) - Skill classification categories +- **SkillDifficulties** (`learning_skill_difficulties`) - Difficulty levels +- **SkillCategoryDifficulties** (`learning_skill_category_difficulties`) - Relationship between skills, categories, and difficulties with learning costs + +### From `model_gsmaster.go`: +- **Skills** (`gsm_skills`) - Character skills +- **Spells** (`gsm_spells`) - Magic spells + +## Excluded Entities + +The following are NOT exported/imported: +- `AuditLogEntry` - Audit logs (transient data) +- `BamortBase` - Base character data (character-specific) +- `BamortCharTrait` - Character traits (character-specific) +- `Magisch` - Character magic data (character-specific) +- `LookupList` - System lookup lists (system config) + +## File Format + +All data is exported to JSON files with indentation for easy reading and editing: + +```json +[ + { + "name": "Skill Name", + "game_system": "midgard", + "source_code": "KOD", + ... + } +] +``` + +## Natural Keys + +Instead of database IDs, the following natural keys are used to identify records: + +| Entity | Natural Key | +|--------|------------| +| Source | `code` | +| Skill | `name` + `game_system` | +| Spell | `name` + `game_system` | +| SkillCategory | `name` + `game_system` | +| SkillDifficulty | `name` + `game_system` | +| SkillCategoryDifficulty | `skill_name` + `skill_system` + `category_name` + `category_system` + `difficulty_name` + `difficulty_system` | + +## Usage + +### Export + +```go +import "bamort/gsmaster" + +// Export specific entity types +err := gsmaster.ExportSources("/path/to/output") +err := gsmaster.ExportSkills("/path/to/output") +err := gsmaster.ExportSpells("/path/to/output") +err := gsmaster.ExportSkillCategories("/path/to/output") +err := gsmaster.ExportSkillDifficulties("/path/to/output") +err := gsmaster.ExportSkillCategoryDifficulties("/path/to/output") + +// Export all master data at once +err := gsmaster.ExportAll("/path/to/output") +``` + +### Import + +```go +import "bamort/gsmaster" + +// Import specific entity types +err := gsmaster.ImportSources("/path/to/input") +err := gsmaster.ImportSkills("/path/to/input") +err := gsmaster.ImportSpells("/path/to/input") +err := gsmaster.ImportSkillCategories("/path/to/input") +err := gsmaster.ImportSkillDifficulties("/path/to/input") +err := gsmaster.ImportSkillCategoryDifficulties("/path/to/input") + +// Import all master data at once +err := gsmaster.ImportAll("/path/to/input") +``` + +## Import Behavior + +The import mechanism follows an "upsert" pattern: + +1. **Check if record exists** using natural keys +2. If **not found**: Create new record +3. If **found**: Update existing record with imported values + +This allows for: +- Importing new data +- Updating existing data +- Safe re-import of previously exported data + +## Dependency Order + +When using `ExportAll()` and `ImportAll()`, entities are processed in dependency order: + +**Export/Import Order:** +1. Sources (no dependencies) +2. SkillCategories (depends on Sources) +3. SkillDifficulties (no dependencies) +4. Skills (depends on Sources) +5. SkillCategoryDifficulties (depends on Skills, Categories, Difficulties) +6. Spells (depends on Sources) + +## File Names + +Each entity type is exported to its own file: + +| Entity | Filename | +|--------|----------| +| Sources | `sources.json` | +| Skills | `skills.json` | +| Spells | `spells.json` | +| SkillCategories | `skill_categories.json` | +| SkillDifficulties | `skill_difficulties.json` | +| SkillCategoryDifficulties | `skill_category_difficulties.json` | + +## Example Workflow + +### Exporting Data + +```go +package main + +import ( + "bamort/gsmaster" + "log" +) + +func main() { + outputDir := "./exported_data" + + if err := gsmaster.ExportAll(outputDir); err != nil { + log.Fatalf("Export failed: %v", err) + } + + log.Println("All master data exported to", outputDir) +} +``` + +### Editing Exported Data + +```bash +# Edit the JSON files manually +vim exported_data/skills.json +``` + +### Importing Modified Data + +```go +package main + +import ( + "bamort/gsmaster" + "bamort/database" + "log" +) + +func main() { + // Initialize database connection + database.InitDB() + + inputDir := "./exported_data" + + if err := gsmaster.ImportAll(inputDir); err != nil { + log.Fatalf("Import failed: %v", err) + } + + log.Println("All master data imported from", inputDir) +} +``` + +## Error Handling + +All export/import functions return errors that should be checked: + +- **Export errors**: Usually file system issues (permissions, disk space) +- **Import errors**: Can be: + - File not found + - Invalid JSON format + - Missing dependencies (e.g., referenced source doesn't exist) + - Database constraint violations + +## Testing + +The export/import mechanism is fully tested with TDD. See `gsmaster/export_import_test.go` for comprehensive test coverage including: + +- Export creates valid JSON files +- Import creates new records +- Import updates existing records +- Relationships are correctly restored using natural keys +- Full export/import cycle works correctly + +## Notes + +- **No Handlers/Routes**: This functionality is intentionally not exposed as API endpoints. Use it programmatically or via CLI tools. +- **ID Independence**: Exported data does not contain database IDs, making it portable across different database instances. +- **Idempotent**: Import can be run multiple times safely - it will update existing records rather than creating duplicates. +- **Transaction Safety**: Each import operation should ideally be wrapped in a database transaction for atomicity. diff --git a/backend/gsmaster/export_import.go b/backend/gsmaster/export_import.go new file mode 100644 index 0000000..8038e4b --- /dev/null +++ b/backend/gsmaster/export_import.go @@ -0,0 +1,1258 @@ +package gsmaster + +import ( + "bamort/database" + "bamort/models" + "encoding/json" + "fmt" + "os" + "path/filepath" + + "gorm.io/gorm" +) + +// ExportableSkill represents a skill without database IDs for export +type ExportableSkill struct { + Name string `json:"name"` + GameSystem string `json:"game_system"` + Beschreibung string `json:"beschreibung"` + SourceCode string `json:"source_code"` // Instead of SourceID + PageNumber int `json:"page_number"` + Initialwert int `json:"initialwert"` + BasisWert int `json:"basiswert"` + Bonuseigenschaft string `json:"bonuseigenschaft"` + Improvable bool `json:"improvable"` + InnateSkill bool `json:"innate_skill"` + Category string `json:"category"` + Difficulty string `json:"difficulty"` +} + +// ExportableSource represents a source for export +type ExportableSource struct { + Code string `json:"code"` + Name string `json:"name"` + FullName string `json:"full_name"` + Edition string `json:"edition"` + Publisher string `json:"publisher"` + PublishYear int `json:"publish_year"` + Description string `json:"description"` + IsCore bool `json:"is_core"` + IsActive bool `json:"is_active"` + GameSystem string `json:"game_system"` +} + +// ExportableSkillCategory represents a skill category for export +type ExportableSkillCategory struct { + Name string `json:"name"` + GameSystem string `json:"game_system"` + SourceCode string `json:"source_code"` +} + +// ExportableSkillDifficulty represents a skill difficulty for export +type ExportableSkillDifficulty struct { + Name string `json:"name"` + GameSystem string `json:"game_system"` +} + +// ExportableSkillCategoryDifficulty represents the relationship for export +type ExportableSkillCategoryDifficulty struct { + SkillName string `json:"skill_name"` + SkillSystem string `json:"skill_system"` + CategoryName string `json:"category_name"` + CategorySystem string `json:"category_system"` + DifficultyName string `json:"difficulty_name"` + DifficultySystem string `json:"difficulty_system"` + LearnCost int `json:"learn_cost"` +} + +// ExportableSpell represents a spell for export +type ExportableSpell struct { + Name string `json:"name"` + GameSystem string `json:"game_system"` + Beschreibung string `json:"beschreibung"` + SourceCode string `json:"source_code"` + PageNumber int `json:"page_number"` + Bonus int `json:"bonus"` + Stufe int `json:"level"` + AP string `json:"ap"` + Art string `json:"art"` + Zauberdauer string `json:"zauberdauer"` + Reichweite string `json:"reichweite"` + Wirkungsziel string `json:"wirkungsziel"` + Wirkungsbereich string `json:"wirkungsbereich"` + Wirkungsdauer string `json:"wirkungsdauer"` + Ursprung string `json:"ursprung"` + Category string `json:"category"` + LearningCategory string `json:"learning_category"` +} + +// ExportableCharacterClass represents a character class for export +type ExportableCharacterClass struct { + Code string `json:"code"` + Name string `json:"name"` + Description string `json:"description"` + SourceCode string `json:"source_code"` + GameSystem string `json:"game_system"` +} + +// ExportableSpellSchool represents a spell school for export +type ExportableSpellSchool struct { + Name string `json:"name"` + Description string `json:"description"` + SourceCode string `json:"source_code"` + GameSystem string `json:"game_system"` +} + +// ExportableClassCategoryEPCost represents class-category EP costs for export +type ExportableClassCategoryEPCost struct { + CharacterClassCode string `json:"character_class_code"` + SkillCategoryName string `json:"skill_category_name"` + EPPerTE int `json:"ep_per_te"` +} + +// ExportableClassSpellSchoolEPCost represents class-spell school EP costs for export +type ExportableClassSpellSchoolEPCost struct { + CharacterClassCode string `json:"character_class_code"` + SpellSchoolName string `json:"spell_school_name"` + EPPerLE int `json:"ep_per_le"` +} + +// ExportableSpellLevelLECost represents spell level LE costs for export +type ExportableSpellLevelLECost struct { + Level int `json:"level"` + LERequired int `json:"le_required"` + GameSystem string `json:"game_system"` +} + +// ExportableSkillImprovementCost represents skill improvement costs for export +type ExportableSkillImprovementCost struct { + SkillName string `json:"skill_name"` + SkillSystem string `json:"skill_system"` + CategoryName string `json:"category_name"` + CategorySystem string `json:"category_system"` + DifficultyName string `json:"difficulty_name"` + DifficultySystem string `json:"difficulty_system"` + CurrentLevel int `json:"current_level"` + TERequired int `json:"te_required"` +} + +// ExportableWeaponSkill represents a weapon skill for export +type ExportableWeaponSkill struct { + Name string `json:"name"` + GameSystem string `json:"game_system"` + Beschreibung string `json:"beschreibung"` + SourceCode string `json:"source_code"` + PageNumber int `json:"page_number"` + Initialwert int `json:"initialwert"` + BasisWert int `json:"basiswert"` + Bonuseigenschaft string `json:"bonuseigenschaft"` + Improvable bool `json:"improvable"` + InnateSkill bool `json:"innate_skill"` + Category string `json:"category"` + Difficulty string `json:"difficulty"` +} + +// ExportableEquipment represents equipment for export +type ExportableEquipment struct { + Name string `json:"name"` + GameSystem string `json:"game_system"` + Beschreibung string `json:"beschreibung"` + SourceCode string `json:"source_code"` + PageNumber int `json:"page_number"` + Gewicht float64 `json:"gewicht"` + Wert float64 `json:"wert"` + PersonalItem bool `json:"personal_item"` +} + +// ExportableWeapon represents a weapon for export +type ExportableWeapon struct { + Name string `json:"name"` + GameSystem string `json:"game_system"` + Beschreibung string `json:"beschreibung"` + SourceCode string `json:"source_code"` + PageNumber int `json:"page_number"` + Gewicht float64 `json:"gewicht"` + Wert float64 `json:"wert"` + PersonalItem bool `json:"personal_item"` + SkillRequired string `json:"skill_required"` + Damage string `json:"damage"` + RangeNear int `json:"range_near"` + RangeMiddle int `json:"range_middle"` + RangeFar int `json:"range_far"` +} + +// ExportableContainer represents a container for export +type ExportableContainer struct { + Name string `json:"name"` + GameSystem string `json:"game_system"` + Beschreibung string `json:"beschreibung"` + SourceCode string `json:"source_code"` + PageNumber int `json:"page_number"` + Gewicht float64 `json:"gewicht"` + Wert float64 `json:"wert"` + PersonalItem bool `json:"personal_item"` + Tragkraft float64 `json:"tragkraft"` + Volumen float64 `json:"volumen"` +} + +// ExportableTransportation represents transportation for export +type ExportableTransportation struct { + Name string `json:"name"` + GameSystem string `json:"game_system"` + Beschreibung string `json:"beschreibung"` + SourceCode string `json:"source_code"` + PageNumber int `json:"page_number"` + Gewicht float64 `json:"gewicht"` + Wert float64 `json:"wert"` + PersonalItem bool `json:"personal_item"` + Tragkraft float64 `json:"tragkraft"` + Volumen float64 `json:"volumen"` +} + +// ExportableBelieve represents a belief system for export +type ExportableBelieve struct { + Name string `json:"name"` + GameSystem string `json:"game_system"` + Beschreibung string `json:"beschreibung"` + SourceCode string `json:"source_code"` + PageNumber int `json:"page_number"` +} + +// ExportSkills exports all skills to a JSON file +func ExportSkills(outputDir string) error { + var skills []models.Skill + if err := database.DB.Find(&skills).Error; err != nil { + return fmt.Errorf("failed to fetch skills: %w", err) + } + + // Get all sources for mapping + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[uint]string) + for _, s := range sources { + sourceMap[s.ID] = s.Code + } + + // Convert to exportable format + exportable := make([]ExportableSkill, len(skills)) + for i, skill := range skills { + exportable[i] = ExportableSkill{ + Name: skill.Name, + GameSystem: skill.GameSystem, + Beschreibung: skill.Beschreibung, + SourceCode: sourceMap[skill.SourceID], + PageNumber: skill.PageNumber, + Initialwert: skill.Initialwert, + BasisWert: skill.BasisWert, + Bonuseigenschaft: skill.Bonuseigenschaft, + Improvable: skill.Improvable, + InnateSkill: skill.InnateSkill, + Category: skill.Category, + Difficulty: skill.Difficulty, + } + } + + return writeJSON(filepath.Join(outputDir, "skills.json"), exportable) +} + +// ImportSkills imports skills from a JSON file +func ImportSkills(inputDir string) error { + var exportable []ExportableSkill + if err := readJSON(filepath.Join(inputDir, "skills.json"), &exportable); err != nil { + return err + } + + // Get all sources for mapping + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[string]uint) + for _, s := range sources { + sourceMap[s.Code] = s.ID + } + + for _, exp := range exportable { + var skill models.Skill + result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&skill) + + sourceID := sourceMap[exp.SourceCode] + + if result.Error == gorm.ErrRecordNotFound { + // Create new skill + skill = models.Skill{ + Name: exp.Name, + GameSystem: exp.GameSystem, + Beschreibung: exp.Beschreibung, + SourceID: sourceID, + PageNumber: exp.PageNumber, + Initialwert: exp.Initialwert, + BasisWert: exp.BasisWert, + Bonuseigenschaft: exp.Bonuseigenschaft, + Improvable: exp.Improvable, + InnateSkill: exp.InnateSkill, + Category: exp.Category, + Difficulty: exp.Difficulty, + } + if err := database.DB.Create(&skill).Error; err != nil { + return fmt.Errorf("failed to create skill %s: %w", exp.Name, err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query skill %s: %w", exp.Name, result.Error) + } else { + // Update existing skill + skill.Beschreibung = exp.Beschreibung + skill.SourceID = sourceID + skill.PageNumber = exp.PageNumber + skill.Initialwert = exp.Initialwert + skill.BasisWert = exp.BasisWert + skill.Bonuseigenschaft = exp.Bonuseigenschaft + skill.Improvable = exp.Improvable + skill.InnateSkill = exp.InnateSkill + skill.Category = exp.Category + skill.Difficulty = exp.Difficulty + + if err := database.DB.Save(&skill).Error; err != nil { + return fmt.Errorf("failed to update skill %s: %w", exp.Name, err) + } + } + } + + return nil +} + +// ExportSources exports all sources to a JSON file +func ExportSources(outputDir string) error { + var sources []models.Source + if err := database.DB.Find(&sources).Error; err != nil { + return fmt.Errorf("failed to fetch sources: %w", err) + } + + exportable := make([]ExportableSource, len(sources)) + for i, s := range sources { + exportable[i] = ExportableSource{ + Code: s.Code, + Name: s.Name, + FullName: s.FullName, + Edition: s.Edition, + Publisher: s.Publisher, + PublishYear: s.PublishYear, + Description: s.Description, + IsCore: s.IsCore, + IsActive: s.IsActive, + GameSystem: s.GameSystem, + } + } + + return writeJSON(filepath.Join(outputDir, "sources.json"), exportable) +} + +// ImportSources imports sources from a JSON file +func ImportSources(inputDir string) error { + var exportable []ExportableSource + if err := readJSON(filepath.Join(inputDir, "sources.json"), &exportable); err != nil { + return err + } + + for _, exp := range exportable { + var source models.Source + result := database.DB.Where("code = ?", exp.Code).First(&source) + + if result.Error == gorm.ErrRecordNotFound { + // Create new source + source = models.Source{ + Code: exp.Code, + Name: exp.Name, + FullName: exp.FullName, + Edition: exp.Edition, + Publisher: exp.Publisher, + PublishYear: exp.PublishYear, + Description: exp.Description, + IsCore: exp.IsCore, + IsActive: exp.IsActive, + GameSystem: exp.GameSystem, + } + if err := database.DB.Create(&source).Error; err != nil { + return fmt.Errorf("failed to create source %s: %w", exp.Code, err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query source %s: %w", exp.Code, result.Error) + } else { + // Update existing source + source.Name = exp.Name + source.FullName = exp.FullName + source.Edition = exp.Edition + source.Publisher = exp.Publisher + source.PublishYear = exp.PublishYear + source.Description = exp.Description + source.IsCore = exp.IsCore + source.IsActive = exp.IsActive + source.GameSystem = exp.GameSystem + + if err := database.DB.Save(&source).Error; err != nil { + return fmt.Errorf("failed to update source %s: %w", exp.Code, err) + } + } + } + + return nil +} + +// ExportSkillCategoryDifficulties exports skill-category-difficulty relationships +func ExportSkillCategoryDifficulties(outputDir string) error { + var scds []models.SkillCategoryDifficulty + if err := database.DB.Find(&scds).Error; err != nil { + return fmt.Errorf("failed to fetch skill category difficulties: %w", err) + } + + exportable := make([]ExportableSkillCategoryDifficulty, len(scds)) + for i, scd := range scds { + var skill models.Skill + var category models.SkillCategory + var difficulty models.SkillDifficulty + + database.DB.First(&skill, scd.SkillID) + database.DB.First(&category, scd.SkillCategoryID) + database.DB.First(&difficulty, scd.SkillDifficultyID) + + exportable[i] = ExportableSkillCategoryDifficulty{ + SkillName: skill.Name, + SkillSystem: skill.GameSystem, + CategoryName: category.Name, + CategorySystem: category.GameSystem, + DifficultyName: difficulty.Name, + DifficultySystem: difficulty.GameSystem, + LearnCost: scd.LearnCost, + } + } + + return writeJSON(filepath.Join(outputDir, "skill_category_difficulties.json"), exportable) +} + +// ImportSkillCategoryDifficulties imports skill-category-difficulty relationships +func ImportSkillCategoryDifficulties(inputDir string) error { + var exportable []ExportableSkillCategoryDifficulty + if err := readJSON(filepath.Join(inputDir, "skill_category_difficulties.json"), &exportable); err != nil { + return err + } + + for _, exp := range exportable { + // Find the skill + var skill models.Skill + if err := database.DB.Where("name = ? AND game_system = ?", exp.SkillName, exp.SkillSystem).First(&skill).Error; err != nil { + return fmt.Errorf("skill not found: %s/%s", exp.SkillName, exp.SkillSystem) + } + + // Find the category + var category models.SkillCategory + if err := database.DB.Where("name = ? AND game_system = ?", exp.CategoryName, exp.CategorySystem).First(&category).Error; err != nil { + return fmt.Errorf("category not found: %s/%s", exp.CategoryName, exp.CategorySystem) + } + + // Find the difficulty + var difficulty models.SkillDifficulty + if err := database.DB.Where("name = ? AND game_system = ?", exp.DifficultyName, exp.DifficultySystem).First(&difficulty).Error; err != nil { + return fmt.Errorf("difficulty not found: %s/%s", exp.DifficultyName, exp.DifficultySystem) + } + + // Check if relationship exists + var scd models.SkillCategoryDifficulty + result := database.DB.Where("skill_id = ? AND skill_category_id = ? AND skill_difficulty_id = ?", + skill.ID, category.ID, difficulty.ID).First(&scd) + + if result.Error == gorm.ErrRecordNotFound { + // Create new relationship + scd = models.SkillCategoryDifficulty{ + SkillID: skill.ID, + SkillCategoryID: category.ID, + SkillDifficultyID: difficulty.ID, + LearnCost: exp.LearnCost, + SCategory: category.Name, + SDifficulty: difficulty.Name, + } + if err := database.DB.Create(&scd).Error; err != nil { + return fmt.Errorf("failed to create relationship: %w", err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query relationship: %w", result.Error) + } else { + // Update existing relationship + scd.LearnCost = exp.LearnCost + scd.SCategory = category.Name + scd.SDifficulty = difficulty.Name + + if err := database.DB.Save(&scd).Error; err != nil { + return fmt.Errorf("failed to update relationship: %w", err) + } + } + } + + return nil +} + +// Helper functions for JSON I/O +func writeJSON(filename string, data interface{}) error { + file, err := os.Create(filename) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", filename, err) + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(data); err != nil { + return fmt.Errorf("failed to encode JSON to %s: %w", filename, err) + } + + return nil +} + +func readJSON(filename string, data interface{}) error { + file, err := os.Open(filename) + if err != nil { + return fmt.Errorf("failed to open file %s: %w", filename, err) + } + defer file.Close() + + decoder := json.NewDecoder(file) + if err := decoder.Decode(data); err != nil { + return fmt.Errorf("failed to decode JSON from %s: %w", filename, err) + } + + return nil +} + +// ExportSkillCategories exports all skill categories to a JSON file +func ExportSkillCategories(outputDir string) error { + var categories []models.SkillCategory + if err := database.DB.Find(&categories).Error; err != nil { + return fmt.Errorf("failed to fetch skill categories: %w", err) + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[uint]string) + for _, s := range sources { + sourceMap[s.ID] = s.Code + } + + exportable := make([]ExportableSkillCategory, len(categories)) + for i, cat := range categories { + exportable[i] = ExportableSkillCategory{ + Name: cat.Name, + GameSystem: cat.GameSystem, + SourceCode: sourceMap[cat.SourceID], + } + } + + return writeJSON(filepath.Join(outputDir, "skill_categories.json"), exportable) +} + +// ImportSkillCategories imports skill categories from a JSON file +func ImportSkillCategories(inputDir string) error { + var exportable []ExportableSkillCategory + if err := readJSON(filepath.Join(inputDir, "skill_categories.json"), &exportable); err != nil { + return err + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[string]uint) + for _, s := range sources { + sourceMap[s.Code] = s.ID + } + + for _, exp := range exportable { + var category models.SkillCategory + result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&category) + + sourceID := sourceMap[exp.SourceCode] + + if result.Error == gorm.ErrRecordNotFound { + category = models.SkillCategory{ + Name: exp.Name, + GameSystem: exp.GameSystem, + SourceID: sourceID, + } + if err := database.DB.Create(&category).Error; err != nil { + return fmt.Errorf("failed to create category %s: %w", exp.Name, err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query category %s: %w", exp.Name, result.Error) + } else { + category.SourceID = sourceID + if err := database.DB.Save(&category).Error; err != nil { + return fmt.Errorf("failed to update category %s: %w", exp.Name, err) + } + } + } + + return nil +} + +// ExportSkillDifficulties exports all skill difficulties to a JSON file +func ExportSkillDifficulties(outputDir string) error { + var difficulties []models.SkillDifficulty + if err := database.DB.Find(&difficulties).Error; err != nil { + return fmt.Errorf("failed to fetch skill difficulties: %w", err) + } + + exportable := make([]ExportableSkillDifficulty, len(difficulties)) + for i, diff := range difficulties { + exportable[i] = ExportableSkillDifficulty{ + Name: diff.Name, + GameSystem: diff.GameSystem, + } + } + + return writeJSON(filepath.Join(outputDir, "skill_difficulties.json"), exportable) +} + +// ImportSkillDifficulties imports skill difficulties from a JSON file +func ImportSkillDifficulties(inputDir string) error { + var exportable []ExportableSkillDifficulty + if err := readJSON(filepath.Join(inputDir, "skill_difficulties.json"), &exportable); err != nil { + return err + } + + for _, exp := range exportable { + var difficulty models.SkillDifficulty + result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&difficulty) + + if result.Error == gorm.ErrRecordNotFound { + difficulty = models.SkillDifficulty{ + Name: exp.Name, + GameSystem: exp.GameSystem, + } + if err := database.DB.Create(&difficulty).Error; err != nil { + return fmt.Errorf("failed to create difficulty %s: %w", exp.Name, err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query difficulty %s: %w", exp.Name, result.Error) + } + // No update needed for difficulties (only name and game_system) + } + + return nil +} + +// ExportSpells exports all spells to a JSON file +func ExportSpells(outputDir string) error { + var spells []models.Spell + if err := database.DB.Find(&spells).Error; err != nil { + return fmt.Errorf("failed to fetch spells: %w", err) + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[uint]string) + for _, s := range sources { + sourceMap[s.ID] = s.Code + } + + exportable := make([]ExportableSpell, len(spells)) + for i, spell := range spells { + exportable[i] = ExportableSpell{ + Name: spell.Name, + GameSystem: spell.GameSystem, + Beschreibung: spell.Beschreibung, + SourceCode: sourceMap[spell.SourceID], + PageNumber: spell.PageNumber, + Bonus: spell.Bonus, + Stufe: spell.Stufe, + AP: spell.AP, + Art: spell.Art, + Zauberdauer: spell.Zauberdauer, + Reichweite: spell.Reichweite, + Wirkungsziel: spell.Wirkungsziel, + Wirkungsbereich: spell.Wirkungsbereich, + Wirkungsdauer: spell.Wirkungsdauer, + Ursprung: spell.Ursprung, + Category: spell.Category, + LearningCategory: spell.LearningCategory, + } + } + + return writeJSON(filepath.Join(outputDir, "spells.json"), exportable) +} + +// ImportSpells imports spells from a JSON file +func ImportSpells(inputDir string) error { + var exportable []ExportableSpell + if err := readJSON(filepath.Join(inputDir, "spells.json"), &exportable); err != nil { + return err + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[string]uint) + for _, s := range sources { + sourceMap[s.Code] = s.ID + } + + for _, exp := range exportable { + var spell models.Spell + result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&spell) + + sourceID := sourceMap[exp.SourceCode] + + if result.Error == gorm.ErrRecordNotFound { + spell = models.Spell{ + Name: exp.Name, + GameSystem: exp.GameSystem, + Beschreibung: exp.Beschreibung, + SourceID: sourceID, + PageNumber: exp.PageNumber, + Bonus: exp.Bonus, + Stufe: exp.Stufe, + AP: exp.AP, + Art: exp.Art, + Zauberdauer: exp.Zauberdauer, + Reichweite: exp.Reichweite, + Wirkungsziel: exp.Wirkungsziel, + Wirkungsbereich: exp.Wirkungsbereich, + Wirkungsdauer: exp.Wirkungsdauer, + Ursprung: exp.Ursprung, + Category: exp.Category, + LearningCategory: exp.LearningCategory, + } + if err := database.DB.Create(&spell).Error; err != nil { + return fmt.Errorf("failed to create spell %s: %w", exp.Name, err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query spell %s: %w", exp.Name, result.Error) + } else { + // Update existing spell + spell.Beschreibung = exp.Beschreibung + spell.SourceID = sourceID + spell.PageNumber = exp.PageNumber + spell.Bonus = exp.Bonus + spell.Stufe = exp.Stufe + spell.AP = exp.AP + spell.Art = exp.Art + spell.Zauberdauer = exp.Zauberdauer + spell.Reichweite = exp.Reichweite + spell.Wirkungsziel = exp.Wirkungsziel + spell.Wirkungsbereich = exp.Wirkungsbereich + spell.Wirkungsdauer = exp.Wirkungsdauer + spell.Ursprung = exp.Ursprung + spell.Category = exp.Category + spell.LearningCategory = exp.LearningCategory + + if err := database.DB.Save(&spell).Error; err != nil { + return fmt.Errorf("failed to update spell %s: %w", exp.Name, err) + } + } + } + + return nil +} + +// ExportCharacterClasses exports all character classes to a JSON file +func ExportCharacterClasses(outputDir string) error { + var classes []models.CharacterClass + if err := database.DB.Find(&classes).Error; err != nil { + return fmt.Errorf("failed to fetch character classes: %w", err) + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[uint]string) + for _, s := range sources { + sourceMap[s.ID] = s.Code + } + + exportable := make([]ExportableCharacterClass, len(classes)) + for i, class := range classes { + exportable[i] = ExportableCharacterClass{ + Code: class.Code, + Name: class.Name, + Description: class.Description, + SourceCode: sourceMap[class.SourceID], + GameSystem: class.GameSystem, + } + } + + return writeJSON(filepath.Join(outputDir, "character_classes.json"), exportable) +} + +// ImportCharacterClasses imports character classes from a JSON file +func ImportCharacterClasses(inputDir string) error { + var exportable []ExportableCharacterClass + if err := readJSON(filepath.Join(inputDir, "character_classes.json"), &exportable); err != nil { + return err + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[string]uint) + for _, s := range sources { + sourceMap[s.Code] = s.ID + } + + for _, exp := range exportable { + var class models.CharacterClass + result := database.DB.Where("code = ?", exp.Code).First(&class) + + sourceID := sourceMap[exp.SourceCode] + + if result.Error == gorm.ErrRecordNotFound { + class = models.CharacterClass{ + Code: exp.Code, + Name: exp.Name, + Description: exp.Description, + SourceID: sourceID, + GameSystem: exp.GameSystem, + } + if err := database.DB.Create(&class).Error; err != nil { + return fmt.Errorf("failed to create character class %s: %w", exp.Code, err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query character class %s: %w", exp.Code, result.Error) + } else { + class.Name = exp.Name + class.Description = exp.Description + class.SourceID = sourceID + class.GameSystem = exp.GameSystem + + if err := database.DB.Save(&class).Error; err != nil { + return fmt.Errorf("failed to update character class %s: %w", exp.Code, err) + } + } + } + + return nil +} + +// ExportSpellSchools exports all spell schools to a JSON file +func ExportSpellSchools(outputDir string) error { + var schools []models.SpellSchool + if err := database.DB.Find(&schools).Error; err != nil { + return fmt.Errorf("failed to fetch spell schools: %w", err) + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[uint]string) + for _, s := range sources { + sourceMap[s.ID] = s.Code + } + + exportable := make([]ExportableSpellSchool, len(schools)) + for i, school := range schools { + exportable[i] = ExportableSpellSchool{ + Name: school.Name, + Description: school.Description, + SourceCode: sourceMap[school.SourceID], + GameSystem: school.GameSystem, + } + } + + return writeJSON(filepath.Join(outputDir, "spell_schools.json"), exportable) +} + +// ImportSpellSchools imports spell schools from a JSON file +func ImportSpellSchools(inputDir string) error { + var exportable []ExportableSpellSchool + if err := readJSON(filepath.Join(inputDir, "spell_schools.json"), &exportable); err != nil { + return err + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[string]uint) + for _, s := range sources { + sourceMap[s.Code] = s.ID + } + + for _, exp := range exportable { + var school models.SpellSchool + result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&school) + + sourceID := sourceMap[exp.SourceCode] + + if result.Error == gorm.ErrRecordNotFound { + school = models.SpellSchool{ + Name: exp.Name, + Description: exp.Description, + SourceID: sourceID, + GameSystem: exp.GameSystem, + } + if err := database.DB.Create(&school).Error; err != nil { + return fmt.Errorf("failed to create spell school %s: %w", exp.Name, err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query spell school %s: %w", exp.Name, result.Error) + } else { + school.Description = exp.Description + school.SourceID = sourceID + school.GameSystem = exp.GameSystem + + if err := database.DB.Save(&school).Error; err != nil { + return fmt.Errorf("failed to update spell school %s: %w", exp.Name, err) + } + } + } + + return nil +} + +// Export/Import functions for ClassCategoryEPCost, ClassSpellSchoolEPCost, SpellLevelLECost +// SkillImprovementCost, WeaponSkill, Equipment, Weapon, Container, Transportation, Believe + +// ExportClassCategoryEPCosts exports class-category EP costs +func ExportClassCategoryEPCosts(outputDir string) error { + var costs []models.ClassCategoryEPCost + if err := database.DB.Find(&costs).Error; err != nil { + return fmt.Errorf("failed to fetch class category EP costs: %w", err) + } + + var classes []models.CharacterClass + database.DB.Find(&classes) + classMap := make(map[uint]string) + for _, c := range classes { + classMap[c.ID] = c.Code + } + + var categories []models.SkillCategory + database.DB.Find(&categories) + categoryMap := make(map[uint]string) + for _, cat := range categories { + categoryMap[cat.ID] = cat.Name + } + + exportable := make([]ExportableClassCategoryEPCost, len(costs)) + for i, cost := range costs { + exportable[i] = ExportableClassCategoryEPCost{ + CharacterClassCode: classMap[cost.CharacterClassID], + SkillCategoryName: categoryMap[cost.SkillCategoryID], + EPPerTE: cost.EPPerTE, + } + } + + return writeJSON(filepath.Join(outputDir, "class_category_ep_costs.json"), exportable) +} + +// ImportClassCategoryEPCosts imports class-category EP costs +func ImportClassCategoryEPCosts(inputDir string) error { + var exportable []ExportableClassCategoryEPCost + if err := readJSON(filepath.Join(inputDir, "class_category_ep_costs.json"), &exportable); err != nil { + return err + } + + var classes []models.CharacterClass + database.DB.Find(&classes) + classMap := make(map[string]uint) + for _, c := range classes { + classMap[c.Code] = c.ID + } + + var categories []models.SkillCategory + database.DB.Find(&categories) + categoryMap := make(map[string]uint) + for _, cat := range categories { + categoryMap[cat.Name] = cat.ID + } + + for _, exp := range exportable { + classID := classMap[exp.CharacterClassCode] + categoryID := categoryMap[exp.SkillCategoryName] + + var cost models.ClassCategoryEPCost + result := database.DB.Where("character_class_id = ? AND skill_category_id = ?", classID, categoryID).First(&cost) + + if result.Error == gorm.ErrRecordNotFound { + cost = models.ClassCategoryEPCost{ + CharacterClassID: classID, + SkillCategoryID: categoryID, + EPPerTE: exp.EPPerTE, + CCLass: exp.CharacterClassCode, + SCategory: exp.SkillCategoryName, + } + if err := database.DB.Create(&cost).Error; err != nil { + return fmt.Errorf("failed to create class category EP cost: %w", err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query class category EP cost: %w", result.Error) + } else { + cost.EPPerTE = exp.EPPerTE + cost.CCLass = exp.CharacterClassCode + cost.SCategory = exp.SkillCategoryName + if err := database.DB.Save(&cost).Error; err != nil { + return fmt.Errorf("failed to update class category EP cost: %w", err) + } + } + } + + return nil +} + +// ExportClassSpellSchoolEPCosts exports class-spell school EP costs +func ExportClassSpellSchoolEPCosts(outputDir string) error { + var costs []models.ClassSpellSchoolEPCost + if err := database.DB.Find(&costs).Error; err != nil { + return fmt.Errorf("failed to fetch class spell school EP costs: %w", err) + } + + var classes []models.CharacterClass + database.DB.Find(&classes) + classMap := make(map[uint]string) + for _, c := range classes { + classMap[c.ID] = c.Code + } + + var schools []models.SpellSchool + database.DB.Find(&schools) + schoolMap := make(map[uint]string) + for _, s := range schools { + schoolMap[s.ID] = s.Name + } + + exportable := make([]ExportableClassSpellSchoolEPCost, len(costs)) + for i, cost := range costs { + exportable[i] = ExportableClassSpellSchoolEPCost{ + CharacterClassCode: classMap[cost.CharacterClassID], + SpellSchoolName: schoolMap[cost.SpellSchoolID], + EPPerLE: cost.EPPerLE, + } + } + + return writeJSON(filepath.Join(outputDir, "class_spell_school_ep_costs.json"), exportable) +} + +// ImportClassSpellSchoolEPCosts imports class-spell school EP costs +func ImportClassSpellSchoolEPCosts(inputDir string) error { + var exportable []ExportableClassSpellSchoolEPCost + if err := readJSON(filepath.Join(inputDir, "class_spell_school_ep_costs.json"), &exportable); err != nil { + return err + } + + var classes []models.CharacterClass + database.DB.Find(&classes) + classMap := make(map[string]uint) + for _, c := range classes { + classMap[c.Code] = c.ID + } + + var schools []models.SpellSchool + database.DB.Find(&schools) + schoolMap := make(map[string]uint) + for _, s := range schools { + schoolMap[s.Name] = s.ID + } + + for _, exp := range exportable { + classID := classMap[exp.CharacterClassCode] + schoolID := schoolMap[exp.SpellSchoolName] + + var cost models.ClassSpellSchoolEPCost + result := database.DB.Where("character_class_id = ? AND spell_school_id = ?", classID, schoolID).First(&cost) + + if result.Error == gorm.ErrRecordNotFound { + cost = models.ClassSpellSchoolEPCost{ + CharacterClassID: classID, + SpellSchoolID: schoolID, + EPPerLE: exp.EPPerLE, + CCLass: exp.CharacterClassCode, + SCategory: exp.SpellSchoolName, + } + if err := database.DB.Create(&cost).Error; err != nil { + return fmt.Errorf("failed to create class spell school EP cost: %w", err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query class spell school EP cost: %w", result.Error) + } else { + cost.EPPerLE = exp.EPPerLE + cost.CCLass = exp.CharacterClassCode + cost.SCategory = exp.SpellSchoolName + if err := database.DB.Save(&cost).Error; err != nil { + return fmt.Errorf("failed to update class spell school EP cost: %w", err) + } + } + } + + return nil +} + +// ExportSpellLevelLECosts exports spell level LE costs +func ExportSpellLevelLECosts(outputDir string) error { + var costs []models.SpellLevelLECost + if err := database.DB.Find(&costs).Error; err != nil { + return fmt.Errorf("failed to fetch spell level LE costs: %w", err) + } + + exportable := make([]ExportableSpellLevelLECost, len(costs)) + for i, cost := range costs { + exportable[i] = ExportableSpellLevelLECost{ + Level: cost.Level, + LERequired: cost.LERequired, + GameSystem: cost.GameSystem, + } + } + + return writeJSON(filepath.Join(outputDir, "spell_level_le_costs.json"), exportable) +} + +// ImportSpellLevelLECosts imports spell level LE costs +func ImportSpellLevelLECosts(inputDir string) error { + var exportable []ExportableSpellLevelLECost + if err := readJSON(filepath.Join(inputDir, "spell_level_le_costs.json"), &exportable); err != nil { + return err + } + + for _, exp := range exportable { + var cost models.SpellLevelLECost + result := database.DB.Where("level = ? AND game_system = ?", exp.Level, exp.GameSystem).First(&cost) + + if result.Error == gorm.ErrRecordNotFound { + cost = models.SpellLevelLECost{ + Level: exp.Level, + LERequired: exp.LERequired, + GameSystem: exp.GameSystem, + } + if err := database.DB.Create(&cost).Error; err != nil { + return fmt.Errorf("failed to create spell level LE cost: %w", err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query spell level LE cost: %w", result.Error) + } else { + cost.LERequired = exp.LERequired + if err := database.DB.Save(&cost).Error; err != nil { + return fmt.Errorf("failed to update spell level LE cost: %w", err) + } + } + } + + return nil +} + +// Note: SkillImprovementCost, WeaponSkill, Equipment, Weapon, Container, Transportation, Believe +// export/import functions follow similar patterns - implement as needed + +// ExportAll exports all master data to the specified directory +func ExportAll(outputDir string) error { + // Export in dependency order + if err := ExportSources(outputDir); err != nil { + return err + } + if err := ExportCharacterClasses(outputDir); err != nil { + return err + } + if err := ExportSkillCategories(outputDir); err != nil { + return err + } + if err := ExportSkillDifficulties(outputDir); err != nil { + return err + } + if err := ExportSpellSchools(outputDir); err != nil { + return err + } + if err := ExportSkills(outputDir); err != nil { + return err + } + if err := ExportSkillCategoryDifficulties(outputDir); err != nil { + return err + } + if err := ExportSpells(outputDir); err != nil { + return err + } + if err := ExportClassCategoryEPCosts(outputDir); err != nil { + return err + } + if err := ExportClassSpellSchoolEPCosts(outputDir); err != nil { + return err + } + if err := ExportSpellLevelLECosts(outputDir); err != nil { + return err + } + if err := ExportSkillImprovementCosts(outputDir); err != nil { + return err + } + if err := ExportWeaponSkills(outputDir); err != nil { + return err + } + if err := ExportEquipment(outputDir); err != nil { + return err + } + if err := ExportWeapons(outputDir); err != nil { + return err + } + if err := ExportContainers(outputDir); err != nil { + return err + } + if err := ExportTransportation(outputDir); err != nil { + return err + } + if err := ExportBelieves(outputDir); err != nil { + return err + } + + return nil +} + +// ImportAll imports all master data from the specified directory +func ImportAll(inputDir string) error { + // Import in dependency order + if err := ImportSources(inputDir); err != nil { + return err + } + if err := ImportCharacterClasses(inputDir); err != nil { + return err + } + if err := ImportSkillCategories(inputDir); err != nil { + return err + } + if err := ImportSkillDifficulties(inputDir); err != nil { + return err + } + if err := ImportSpellSchools(inputDir); err != nil { + return err + } + if err := ImportSkills(inputDir); err != nil { + return err + } + if err := ImportSkillCategoryDifficulties(inputDir); err != nil { + return err + } + if err := ImportSpells(inputDir); err != nil { + return err + } + if err := ImportClassCategoryEPCosts(inputDir); err != nil { + return err + } + if err := ImportClassSpellSchoolEPCosts(inputDir); err != nil { + return err + } + if err := ImportSpellLevelLECosts(inputDir); err != nil { + return err + } + if err := ImportSkillImprovementCosts(inputDir); err != nil { + return err + } + if err := ImportWeaponSkills(inputDir); err != nil { + return err + } + if err := ImportEquipment(inputDir); err != nil { + return err + } + if err := ImportWeapons(inputDir); err != nil { + return err + } + if err := ImportContainers(inputDir); err != nil { + return err + } + if err := ImportTransportation(inputDir); err != nil { + return err + } + if err := ImportBelieves(inputDir); err != nil { + return err + } + + return nil +} diff --git a/backend/gsmaster/export_import_additional.go b/backend/gsmaster/export_import_additional.go new file mode 100644 index 0000000..0b72fb8 --- /dev/null +++ b/backend/gsmaster/export_import_additional.go @@ -0,0 +1,687 @@ +package gsmaster + +import ( + "bamort/database" + "bamort/models" + "fmt" + "path/filepath" + + "gorm.io/gorm" +) + +// ExportSkillImprovementCosts exports all skill improvement costs to a JSON file +func ExportSkillImprovementCosts(outputDir string) error { + var costs []models.SkillImprovementCost + if err := database.DB.Find(&costs).Error; err != nil { + return fmt.Errorf("failed to fetch skill improvement costs: %w", err) + } + + // Build maps for skill category difficulties + var scds []models.SkillCategoryDifficulty + database.DB.Find(&scds) + scdMap := make(map[uint]models.SkillCategoryDifficulty) + for _, scd := range scds { + scdMap[scd.ID] = scd + } + + // Get skills + var skills []models.Skill + database.DB.Find(&skills) + skillMap := make(map[uint]models.Skill) + for _, s := range skills { + skillMap[s.ID] = s + } + + // Get categories + var categories []models.SkillCategory + database.DB.Find(&categories) + categoryMap := make(map[uint]models.SkillCategory) + for _, c := range categories { + categoryMap[c.ID] = c + } + + // Get difficulties + var difficulties []models.SkillDifficulty + database.DB.Find(&difficulties) + difficultyMap := make(map[uint]models.SkillDifficulty) + for _, d := range difficulties { + difficultyMap[d.ID] = d + } + + exportable := make([]ExportableSkillImprovementCost, len(costs)) + for i, cost := range costs { + scd := scdMap[cost.SkillCategoryDifficultyID] + skill := skillMap[scd.SkillID] + category := categoryMap[scd.SkillCategoryID] + difficulty := difficultyMap[scd.SkillDifficultyID] + + exportable[i] = ExportableSkillImprovementCost{ + SkillName: skill.Name, + SkillSystem: skill.GameSystem, + CategoryName: category.Name, + CategorySystem: category.GameSystem, + DifficultyName: difficulty.Name, + DifficultySystem: difficulty.GameSystem, + CurrentLevel: cost.CurrentLevel, + TERequired: cost.TERequired, + } + } + + return writeJSON(filepath.Join(outputDir, "skill_improvement_costs.json"), exportable) +} + +// ImportSkillImprovementCosts imports skill improvement costs from a JSON file +func ImportSkillImprovementCosts(inputDir string) error { + var exportable []ExportableSkillImprovementCost + if err := readJSON(filepath.Join(inputDir, "skill_improvement_costs.json"), &exportable); err != nil { + return err + } + + for _, exp := range exportable { + // Find skill + var skill models.Skill + if err := database.DB.Where("name = ? AND game_system = ?", exp.SkillName, exp.SkillSystem).First(&skill).Error; err != nil { + return fmt.Errorf("skill not found: %s: %w", exp.SkillName, err) + } + + // Find category + var category models.SkillCategory + if err := database.DB.Where("name = ? AND game_system = ?", exp.CategoryName, exp.CategorySystem).First(&category).Error; err != nil { + return fmt.Errorf("category not found: %s: %w", exp.CategoryName, err) + } + + // Find difficulty + var difficulty models.SkillDifficulty + if err := database.DB.Where("name = ? AND game_system = ?", exp.DifficultyName, exp.DifficultySystem).First(&difficulty).Error; err != nil { + return fmt.Errorf("difficulty not found: %s: %w", exp.DifficultyName, err) + } + + // Find SkillCategoryDifficulty + var scd models.SkillCategoryDifficulty + if err := database.DB.Where("skill_id = ? AND skill_category_id = ? AND skill_difficulty_id = ?", + skill.ID, category.ID, difficulty.ID).First(&scd).Error; err != nil { + return fmt.Errorf("skill category difficulty not found: %w", err) + } + + // Find or create SkillImprovementCost + var cost models.SkillImprovementCost + result := database.DB.Where("skill_category_difficulty_id = ? AND current_level = ?", + scd.ID, exp.CurrentLevel).First(&cost) + + if result.Error == gorm.ErrRecordNotFound { + cost = models.SkillImprovementCost{ + SkillCategoryDifficultyID: scd.ID, + CurrentLevel: exp.CurrentLevel, + TERequired: exp.TERequired, + } + if err := database.DB.Create(&cost).Error; err != nil { + return fmt.Errorf("failed to create skill improvement cost: %w", err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query skill improvement cost: %w", result.Error) + } else { + cost.TERequired = exp.TERequired + if err := database.DB.Save(&cost).Error; err != nil { + return fmt.Errorf("failed to update skill improvement cost: %w", err) + } + } + } + + return nil +} + +// ExportWeaponSkills exports all weapon skills to a JSON file +func ExportWeaponSkills(outputDir string) error { + var skills []models.WeaponSkill + if err := database.DB.Find(&skills).Error; err != nil { + return fmt.Errorf("failed to fetch weapon skills: %w", err) + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[uint]string) + for _, s := range sources { + sourceMap[s.ID] = s.Code + } + + exportable := make([]ExportableWeaponSkill, len(skills)) + for i, skill := range skills { + exportable[i] = ExportableWeaponSkill{ + Name: skill.Name, + GameSystem: skill.GameSystem, + Beschreibung: skill.Beschreibung, + SourceCode: sourceMap[skill.SourceID], + PageNumber: skill.PageNumber, + Initialwert: skill.Initialwert, + BasisWert: skill.BasisWert, + Bonuseigenschaft: skill.Bonuseigenschaft, + Improvable: skill.Improvable, + InnateSkill: skill.InnateSkill, + Category: skill.Category, + Difficulty: skill.Difficulty, + } + } + + return writeJSON(filepath.Join(outputDir, "weapon_skills.json"), exportable) +} + +// ImportWeaponSkills imports weapon skills from a JSON file +func ImportWeaponSkills(inputDir string) error { + var exportable []ExportableWeaponSkill + if err := readJSON(filepath.Join(inputDir, "weapon_skills.json"), &exportable); err != nil { + return err + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[string]uint) + for _, s := range sources { + sourceMap[s.Code] = s.ID + } + + for _, exp := range exportable { + var skill models.WeaponSkill + result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&skill) + + sourceID := sourceMap[exp.SourceCode] + + if result.Error == gorm.ErrRecordNotFound { + skill = models.WeaponSkill{ + Skill: models.Skill{ + Name: exp.Name, + GameSystem: exp.GameSystem, + Beschreibung: exp.Beschreibung, + SourceID: sourceID, + PageNumber: exp.PageNumber, + Initialwert: exp.Initialwert, + BasisWert: exp.BasisWert, + Bonuseigenschaft: exp.Bonuseigenschaft, + Improvable: exp.Improvable, + InnateSkill: exp.InnateSkill, + Category: exp.Category, + Difficulty: exp.Difficulty, + }, + } + if err := database.DB.Create(&skill).Error; err != nil { + return fmt.Errorf("failed to create weapon skill %s: %w", exp.Name, err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query weapon skill %s: %w", exp.Name, result.Error) + } else { + skill.Beschreibung = exp.Beschreibung + skill.SourceID = sourceID + skill.PageNumber = exp.PageNumber + skill.Initialwert = exp.Initialwert + skill.BasisWert = exp.BasisWert + skill.Bonuseigenschaft = exp.Bonuseigenschaft + skill.Improvable = exp.Improvable + skill.InnateSkill = exp.InnateSkill + skill.Category = exp.Category + skill.Difficulty = exp.Difficulty + + if err := database.DB.Save(&skill).Error; err != nil { + return fmt.Errorf("failed to update weapon skill %s: %w", exp.Name, err) + } + } + } + + return nil +} + +// ExportEquipment exports all equipment to a JSON file +func ExportEquipment(outputDir string) error { + var equipment []models.Equipment + if err := database.DB.Find(&equipment).Error; err != nil { + return fmt.Errorf("failed to fetch equipment: %w", err) + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[uint]string) + for _, s := range sources { + sourceMap[s.ID] = s.Code + } + + exportable := make([]ExportableEquipment, len(equipment)) + for i, eq := range equipment { + exportable[i] = ExportableEquipment{ + Name: eq.Name, + GameSystem: eq.GameSystem, + Beschreibung: eq.Beschreibung, + SourceCode: sourceMap[eq.SourceID], + PageNumber: eq.PageNumber, + Gewicht: eq.Gewicht, + Wert: eq.Wert, + PersonalItem: eq.PersonalItem, + } + } + + return writeJSON(filepath.Join(outputDir, "equipment.json"), exportable) +} + +// ImportEquipment imports equipment from a JSON file +func ImportEquipment(inputDir string) error { + var exportable []ExportableEquipment + if err := readJSON(filepath.Join(inputDir, "equipment.json"), &exportable); err != nil { + return err + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[string]uint) + for _, s := range sources { + sourceMap[s.Code] = s.ID + } + + for _, exp := range exportable { + var eq models.Equipment + result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&eq) + + sourceID := sourceMap[exp.SourceCode] + + if result.Error == gorm.ErrRecordNotFound { + eq = models.Equipment{ + Name: exp.Name, + GameSystem: exp.GameSystem, + Beschreibung: exp.Beschreibung, + SourceID: sourceID, + PageNumber: exp.PageNumber, + Gewicht: exp.Gewicht, + Wert: exp.Wert, + PersonalItem: exp.PersonalItem, + } + if err := database.DB.Create(&eq).Error; err != nil { + return fmt.Errorf("failed to create equipment %s: %w", exp.Name, err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query equipment %s: %w", exp.Name, result.Error) + } else { + eq.Beschreibung = exp.Beschreibung + eq.SourceID = sourceID + eq.PageNumber = exp.PageNumber + eq.Gewicht = exp.Gewicht + eq.Wert = exp.Wert + eq.PersonalItem = exp.PersonalItem + + if err := database.DB.Save(&eq).Error; err != nil { + return fmt.Errorf("failed to update equipment %s: %w", exp.Name, err) + } + } + } + + return nil +} + +// ExportWeapons exports all weapons to a JSON file +func ExportWeapons(outputDir string) error { + var weapons []models.Weapon + if err := database.DB.Find(&weapons).Error; err != nil { + return fmt.Errorf("failed to fetch weapons: %w", err) + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[uint]string) + for _, s := range sources { + sourceMap[s.ID] = s.Code + } + + exportable := make([]ExportableWeapon, len(weapons)) + for i, weapon := range weapons { + exportable[i] = ExportableWeapon{ + Name: weapon.Name, + GameSystem: weapon.GameSystem, + Beschreibung: weapon.Beschreibung, + SourceCode: sourceMap[weapon.SourceID], + PageNumber: weapon.PageNumber, + Gewicht: weapon.Gewicht, + Wert: weapon.Wert, + PersonalItem: weapon.PersonalItem, + SkillRequired: weapon.SkillRequired, + Damage: weapon.Damage, + RangeNear: weapon.RangeNear, + RangeMiddle: weapon.RangeMiddle, + RangeFar: weapon.RangeFar, + } + } + + return writeJSON(filepath.Join(outputDir, "weapons.json"), exportable) +} + +// ImportWeapons imports weapons from a JSON file +func ImportWeapons(inputDir string) error { + var exportable []ExportableWeapon + if err := readJSON(filepath.Join(inputDir, "weapons.json"), &exportable); err != nil { + return err + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[string]uint) + for _, s := range sources { + sourceMap[s.Code] = s.ID + } + + for _, exp := range exportable { + var weapon models.Weapon + result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&weapon) + + sourceID := sourceMap[exp.SourceCode] + + if result.Error == gorm.ErrRecordNotFound { + weapon = models.Weapon{ + Equipment: models.Equipment{ + Name: exp.Name, + GameSystem: exp.GameSystem, + Beschreibung: exp.Beschreibung, + SourceID: sourceID, + PageNumber: exp.PageNumber, + Gewicht: exp.Gewicht, + Wert: exp.Wert, + PersonalItem: exp.PersonalItem, + }, + SkillRequired: exp.SkillRequired, + Damage: exp.Damage, + RangeNear: exp.RangeNear, + RangeMiddle: exp.RangeMiddle, + RangeFar: exp.RangeFar, + } + if err := database.DB.Create(&weapon).Error; err != nil { + return fmt.Errorf("failed to create weapon %s: %w", exp.Name, err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query weapon %s: %w", exp.Name, result.Error) + } else { + weapon.Beschreibung = exp.Beschreibung + weapon.SourceID = sourceID + weapon.PageNumber = exp.PageNumber + weapon.Gewicht = exp.Gewicht + weapon.Wert = exp.Wert + weapon.PersonalItem = exp.PersonalItem + weapon.SkillRequired = exp.SkillRequired + weapon.Damage = exp.Damage + weapon.RangeNear = exp.RangeNear + weapon.RangeMiddle = exp.RangeMiddle + weapon.RangeFar = exp.RangeFar + + if err := database.DB.Save(&weapon).Error; err != nil { + return fmt.Errorf("failed to update weapon %s: %w", exp.Name, err) + } + } + } + + return nil +} + +// ExportContainers exports all containers to a JSON file +func ExportContainers(outputDir string) error { + var containers []models.Container + if err := database.DB.Find(&containers).Error; err != nil { + return fmt.Errorf("failed to fetch containers: %w", err) + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[uint]string) + for _, s := range sources { + sourceMap[s.ID] = s.Code + } + + exportable := make([]ExportableContainer, len(containers)) + for i, container := range containers { + exportable[i] = ExportableContainer{ + Name: container.Name, + GameSystem: container.GameSystem, + Beschreibung: container.Beschreibung, + SourceCode: sourceMap[container.SourceID], + PageNumber: container.PageNumber, + Gewicht: container.Gewicht, + Wert: container.Wert, + PersonalItem: container.PersonalItem, + Tragkraft: container.Tragkraft, + Volumen: container.Volumen, + } + } + + return writeJSON(filepath.Join(outputDir, "containers.json"), exportable) +} + +// ImportContainers imports containers from a JSON file +func ImportContainers(inputDir string) error { + var exportable []ExportableContainer + if err := readJSON(filepath.Join(inputDir, "containers.json"), &exportable); err != nil { + return err + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[string]uint) + for _, s := range sources { + sourceMap[s.Code] = s.ID + } + + for _, exp := range exportable { + var container models.Container + result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&container) + + sourceID := sourceMap[exp.SourceCode] + + if result.Error == gorm.ErrRecordNotFound { + container = models.Container{ + Equipment: models.Equipment{ + Name: exp.Name, + GameSystem: exp.GameSystem, + Beschreibung: exp.Beschreibung, + SourceID: sourceID, + PageNumber: exp.PageNumber, + Gewicht: exp.Gewicht, + Wert: exp.Wert, + PersonalItem: exp.PersonalItem, + }, + Tragkraft: exp.Tragkraft, + Volumen: exp.Volumen, + } + if err := database.DB.Create(&container).Error; err != nil { + return fmt.Errorf("failed to create container %s: %w", exp.Name, err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query container %s: %w", exp.Name, result.Error) + } else { + container.Beschreibung = exp.Beschreibung + container.SourceID = sourceID + container.PageNumber = exp.PageNumber + container.Gewicht = exp.Gewicht + container.Wert = exp.Wert + container.PersonalItem = exp.PersonalItem + container.Tragkraft = exp.Tragkraft + container.Volumen = exp.Volumen + + if err := database.DB.Save(&container).Error; err != nil { + return fmt.Errorf("failed to update container %s: %w", exp.Name, err) + } + } + } + + return nil +} + +// ExportTransportation exports all transportation to a JSON file +func ExportTransportation(outputDir string) error { + var transportation []models.Transportation + if err := database.DB.Find(&transportation).Error; err != nil { + return fmt.Errorf("failed to fetch transportation: %w", err) + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[uint]string) + for _, s := range sources { + sourceMap[s.ID] = s.Code + } + + exportable := make([]ExportableTransportation, len(transportation)) + for i, trans := range transportation { + exportable[i] = ExportableTransportation{ + Name: trans.Name, + GameSystem: trans.GameSystem, + Beschreibung: trans.Beschreibung, + SourceCode: sourceMap[trans.SourceID], + PageNumber: trans.PageNumber, + Gewicht: trans.Gewicht, + Wert: trans.Wert, + PersonalItem: trans.PersonalItem, + Tragkraft: trans.Tragkraft, + Volumen: trans.Volumen, + } + } + + return writeJSON(filepath.Join(outputDir, "transportation.json"), exportable) +} + +// ImportTransportation imports transportation from a JSON file +func ImportTransportation(inputDir string) error { + var exportable []ExportableTransportation + if err := readJSON(filepath.Join(inputDir, "transportation.json"), &exportable); err != nil { + return err + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[string]uint) + for _, s := range sources { + sourceMap[s.Code] = s.ID + } + + for _, exp := range exportable { + var trans models.Transportation + result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&trans) + + sourceID := sourceMap[exp.SourceCode] + + if result.Error == gorm.ErrRecordNotFound { + trans = models.Transportation{ + Container: models.Container{ + Equipment: models.Equipment{ + Name: exp.Name, + GameSystem: exp.GameSystem, + Beschreibung: exp.Beschreibung, + SourceID: sourceID, + PageNumber: exp.PageNumber, + Gewicht: exp.Gewicht, + Wert: exp.Wert, + PersonalItem: exp.PersonalItem, + }, + Tragkraft: exp.Tragkraft, + Volumen: exp.Volumen, + }, + } + if err := database.DB.Create(&trans).Error; err != nil { + return fmt.Errorf("failed to create transportation %s: %w", exp.Name, err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query transportation %s: %w", exp.Name, result.Error) + } else { + trans.Beschreibung = exp.Beschreibung + trans.SourceID = sourceID + trans.PageNumber = exp.PageNumber + trans.Gewicht = exp.Gewicht + trans.Wert = exp.Wert + trans.PersonalItem = exp.PersonalItem + trans.Tragkraft = exp.Tragkraft + trans.Volumen = exp.Volumen + + if err := database.DB.Save(&trans).Error; err != nil { + return fmt.Errorf("failed to update transportation %s: %w", exp.Name, err) + } + } + } + + return nil +} + +// ExportBelieves exports all beliefs to a JSON file +func ExportBelieves(outputDir string) error { + var believes []models.Believe + if err := database.DB.Find(&believes).Error; err != nil { + return fmt.Errorf("failed to fetch believes: %w", err) + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[uint]string) + for _, s := range sources { + sourceMap[s.ID] = s.Code + } + + exportable := make([]ExportableBelieve, len(believes)) + for i, believe := range believes { + exportable[i] = ExportableBelieve{ + Name: believe.Name, + GameSystem: believe.GameSystem, + Beschreibung: believe.Beschreibung, + SourceCode: sourceMap[believe.SourceID], + PageNumber: believe.PageNumber, + } + } + + return writeJSON(filepath.Join(outputDir, "believes.json"), exportable) +} + +// ImportBelieves imports believes from a JSON file +func ImportBelieves(inputDir string) error { + var exportable []ExportableBelieve + if err := readJSON(filepath.Join(inputDir, "believes.json"), &exportable); err != nil { + return err + } + + // Get source map + var sources []models.Source + database.DB.Find(&sources) + sourceMap := make(map[string]uint) + for _, s := range sources { + sourceMap[s.Code] = s.ID + } + + for _, exp := range exportable { + var believe models.Believe + result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&believe) + + sourceID := sourceMap[exp.SourceCode] + + if result.Error == gorm.ErrRecordNotFound { + believe = models.Believe{ + Name: exp.Name, + GameSystem: exp.GameSystem, + Beschreibung: exp.Beschreibung, + SourceID: sourceID, + PageNumber: exp.PageNumber, + } + if err := database.DB.Create(&believe).Error; err != nil { + return fmt.Errorf("failed to create believe %s: %w", exp.Name, err) + } + } else if result.Error != nil { + return fmt.Errorf("failed to query believe %s: %w", exp.Name, result.Error) + } else { + believe.Beschreibung = exp.Beschreibung + believe.SourceID = sourceID + believe.PageNumber = exp.PageNumber + + if err := database.DB.Save(&believe).Error; err != nil { + return fmt.Errorf("failed to update believe %s: %w", exp.Name, err) + } + } + } + + return nil +} diff --git a/backend/gsmaster/export_import_test.go b/backend/gsmaster/export_import_test.go new file mode 100644 index 0000000..d0d8928 --- /dev/null +++ b/backend/gsmaster/export_import_test.go @@ -0,0 +1,914 @@ +package gsmaster + +import ( + "bamort/database" + "bamort/models" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "gorm.io/gorm" +) + +func TestExportSkills(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + // Create test data + source := getOrCreateSource("KOD", "Kodex") + skill := models.Skill{ + Name: "Schwimmen", + GameSystem: "midgard", + Beschreibung: "Schwimmen im Wasser", + Initialwert: 12, + BasisWert: 0, + Bonuseigenschaft: "Gw", + Improvable: true, + InnateSkill: false, + SourceID: source.ID, + PageNumber: 42, + } + database.DB.Create(&skill) + + // Export skills + tmpDir := t.TempDir() + err := ExportSkills(tmpDir) + if err != nil { + t.Fatalf("ExportSkills failed: %v", err) + } + + // Verify file exists + exportFile := filepath.Join(tmpDir, "skills.json") + if _, err := os.Stat(exportFile); os.IsNotExist(err) { + t.Fatalf("Export file not created: %s", exportFile) + } +} + +func TestImportSkills(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + // Create source that will be referenced + source := getOrCreateSource("KOD", "Kodex") + + // Export first + tmpDir := t.TempDir() + skill := models.Skill{ + Name: "Klettern", + GameSystem: "midgard", + Beschreibung: "Klettern an Wänden", + Initialwert: 10, + BasisWert: 0, + Bonuseigenschaft: "Gw", + Improvable: true, + InnateSkill: false, + SourceID: source.ID, + PageNumber: 50, + } + database.DB.Create(&skill) + + err := ExportSkills(tmpDir) + if err != nil { + t.Fatalf("ExportSkills failed: %v", err) + } + + // Delete the skill + database.DB.Delete(&skill) + + // Import back + err = ImportSkills(tmpDir) + if err != nil { + t.Fatalf("ImportSkills failed: %v", err) + } + + // Verify skill was imported + var importedSkill models.Skill + err = database.DB.Where("name = ? AND game_system = ?", "Klettern", "midgard").First(&importedSkill).Error + if err != nil { + t.Fatalf("Imported skill not found: %v", err) + } + + if importedSkill.Beschreibung != "Klettern an Wänden" { + t.Errorf("Expected beschreibung 'Klettern an Wänden', got '%s'", importedSkill.Beschreibung) + } + + if importedSkill.Initialwert != 10 { + t.Errorf("Expected initialwert 10, got %d", importedSkill.Initialwert) + } +} + +func TestImportSkillsUpdate(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + source := getOrCreateSource("KOD", "Kodex") + + // Create existing skill + skill := models.Skill{ + Name: "Reiten", + GameSystem: "midgard", + Beschreibung: "Alte Beschreibung", + Initialwert: 8, + SourceID: source.ID, + PageNumber: 30, + } + database.DB.Create(&skill) + + // Export, modify, and re-import + tmpDir := t.TempDir() + err := ExportSkills(tmpDir) + if err != nil { + t.Fatalf("ExportSkills failed: %v", err) + } + + // Update skill manually + skill.Beschreibung = "Neue Beschreibung" + skill.Initialwert = 12 + database.DB.Save(&skill) + + // Export again with updated values + err = ExportSkills(tmpDir) + if err != nil { + t.Fatalf("ExportSkills failed: %v", err) + } + + // Reset to old values + skill.Beschreibung = "Alte Beschreibung" + skill.Initialwert = 8 + database.DB.Save(&skill) + + // Import should update to exported values + err = ImportSkills(tmpDir) + if err != nil { + t.Fatalf("ImportSkills failed: %v", err) + } + + // Verify update + var updatedSkill models.Skill + err = database.DB.Where("name = ? AND game_system = ?", "Reiten", "midgard").First(&updatedSkill).Error + if err != nil { + t.Fatalf("Updated skill not found: %v", err) + } + + if updatedSkill.Beschreibung != "Neue Beschreibung" { + t.Errorf("Expected updated beschreibung 'Neue Beschreibung', got '%s'", updatedSkill.Beschreibung) + } + + if updatedSkill.Initialwert != 12 { + t.Errorf("Expected updated initialwert 12, got %d", updatedSkill.Initialwert) + } +} + +func TestExportImportSources(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + // Create test source + source := models.Source{ + Code: "ARK", + Name: "Arkanum", + GameSystem: "midgard", + IsActive: true, + } + database.DB.Create(&source) + + // Export + tmpDir := t.TempDir() + err := ExportSources(tmpDir) + if err != nil { + t.Fatalf("ExportSources failed: %v", err) + } + + // Delete + database.DB.Delete(&source) + + // Import + err = ImportSources(tmpDir) + if err != nil { + t.Fatalf("ImportSources failed: %v", err) + } + + // Verify + var imported models.Source + err = database.DB.Where("code = ?", "ARK").First(&imported).Error + if err != nil { + t.Fatalf("Imported source not found: %v", err) + } + + if imported.Name != "Arkanum" { + t.Errorf("Expected name 'Arkanum', got '%s'", imported.Name) + } +} + +func TestExportImportSkillCategoryDifficulty(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + // Create dependencies + source := getOrCreateSource("KOD", "Kodex") + skill := models.Skill{ + Name: "Tanzen", + GameSystem: "midgard", + SourceID: source.ID, + } + database.DB.Create(&skill) + + category := getOrCreateCategory("Alltag", source.ID) + difficulty := getOrCreateDifficulty("leicht") + + // Create relationship + scd := models.SkillCategoryDifficulty{ + SkillID: skill.ID, + SkillCategoryID: category.ID, + SkillDifficultyID: difficulty.ID, + LearnCost: 5, + SCategory: category.Name, + SDifficulty: difficulty.Name, + } + database.DB.Create(&scd) + + // Export + tmpDir := t.TempDir() + err := ExportSkillCategoryDifficulties(tmpDir) + if err != nil { + t.Fatalf("ExportSkillCategoryDifficulties failed: %v", err) + } + + // Delete relationship + database.DB.Delete(&scd) + + // Import + err = ImportSkillCategoryDifficulties(tmpDir) + if err != nil { + t.Fatalf("ImportSkillCategoryDifficulties failed: %v", err) + } + + // Verify relationship was recreated + var imported models.SkillCategoryDifficulty + err = database.DB.Where("skill_id = ? AND skill_category_id = ?", skill.ID, category.ID).First(&imported).Error + if err != nil { + t.Fatalf("Imported relationship not found: %v", err) + } + + if imported.LearnCost != 5 { + t.Errorf("Expected learn_cost 5, got %d", imported.LearnCost) + } +} + +func TestExportImportSkillCategories(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + // Create test data + source := getOrCreateSource("TEST_SC", "Test Source") + category := models.SkillCategory{ + Name: "TestCategory", + GameSystem: "midgard", + SourceID: source.ID, + } + database.DB.Create(&category) + + // Export + tempDir := t.TempDir() + err := ExportSkillCategories(tempDir) + if err != nil { + t.Fatalf("ExportSkillCategories failed: %v", err) + } + + // Verify file was created + filename := filepath.Join(tempDir, "skill_categories.json") + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Fatalf("Export file not created: %s", filename) + } + + // Delete the category + database.DB.Unscoped().Delete(&category) + + // Import + err = ImportSkillCategories(tempDir) + if err != nil { + t.Fatalf("ImportSkillCategories failed: %v", err) + } + + // Verify the category was recreated + var imported models.SkillCategory + result := database.DB.Where("name = ? AND game_system = ?", "TestCategory", "midgard").First(&imported) + if result.Error != nil { + t.Fatalf("Category not found after import: %v", result.Error) + } + + if imported.SourceID != source.ID { + t.Errorf("Expected SourceID %d, got %d", source.ID, imported.SourceID) + } +} + +func TestExportImportSkillDifficulties(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + // Create test data + difficulty := models.SkillDifficulty{ + Name: "TestDifficulty", + GameSystem: "midgard", + } + database.DB.Create(&difficulty) + + // Export + tempDir := t.TempDir() + err := ExportSkillDifficulties(tempDir) + if err != nil { + t.Fatalf("ExportSkillDifficulties failed: %v", err) + } + + // Verify file was created + filename := filepath.Join(tempDir, "skill_difficulties.json") + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Fatalf("Export file not created: %s", filename) + } + + // Delete the difficulty + database.DB.Unscoped().Delete(&difficulty) + + // Import + err = ImportSkillDifficulties(tempDir) + if err != nil { + t.Fatalf("ImportSkillDifficulties failed: %v", err) + } + + // Verify the difficulty was recreated + var imported models.SkillDifficulty + result := database.DB.Where("name = ? AND game_system = ?", "TestDifficulty", "midgard").First(&imported) + if result.Error != nil { + t.Fatalf("Difficulty not found after import: %v", result.Error) + } +} + +func TestExportImportSpells(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + // Create test data + source := getOrCreateSource("TEST_SP", "Test Spell Source") + spell := models.Spell{ + Name: "TestSpell", + GameSystem: "midgard", + Beschreibung: "Test description", + SourceID: source.ID, + PageNumber: 42, + Bonus: 5, + Stufe: 3, + AP: "2", + Art: "Gestenzauber", + Zauberdauer: "10 sec", + Reichweite: "10m", + Wirkungsziel: "Person", + Wirkungsbereich: "1 Person", + Wirkungsdauer: "1h", + Ursprung: "Elben", + Category: "normal", + LearningCategory: "default", + } + database.DB.Create(&spell) + + // Export + tempDir := t.TempDir() + err := ExportSpells(tempDir) + if err != nil { + t.Fatalf("ExportSpells failed: %v", err) + } + + // Verify file was created + filename := filepath.Join(tempDir, "spells.json") + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Fatalf("Export file not created: %s", filename) + } + + // Modify the spell + spell.Beschreibung = "Old description" + spell.Bonus = 3 + database.DB.Save(&spell) + + // Import (should update) + err = ImportSpells(tempDir) + if err != nil { + t.Fatalf("ImportSpells failed: %v", err) + } + + // Verify the spell was updated + var imported models.Spell + result := database.DB.Where("name = ? AND game_system = ?", "TestSpell", "midgard").First(&imported) + if result.Error != nil { + t.Fatalf("Spell not found after import: %v", result.Error) + } + + if imported.Beschreibung != "Test description" { + t.Errorf("Expected description 'Test description', got '%s'", imported.Beschreibung) + } + if imported.Bonus != 5 { + t.Errorf("Expected bonus 5, got %d", imported.Bonus) + } + if imported.Stufe != 3 { + t.Errorf("Expected level 3, got %d", imported.Stufe) + } +} + +func TestExportImportAll(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + // Create test data + source := getOrCreateSource("TEST_ALL", "Test All Source") + + category := models.SkillCategory{Name: "AllCategory", GameSystem: "midgard", SourceID: source.ID} + database.DB.Create(&category) + + difficulty := models.SkillDifficulty{Name: "AllDifficulty", GameSystem: "midgard"} + database.DB.Create(&difficulty) + + skill := models.Skill{ + Name: "AllSkill", + GameSystem: "midgard", + SourceID: source.ID, + Initialwert: 10, + } + database.DB.Create(&skill) + + spell := models.Spell{ + Name: "AllSpell", + GameSystem: "midgard", + SourceID: source.ID, + Stufe: 2, + } + database.DB.Create(&spell) + + // Export all + tempDir := t.TempDir() + err := ExportAll(tempDir) + if err != nil { + t.Fatalf("ExportAll failed: %v", err) + } + + // Verify all files were created + files := []string{ + "sources.json", + "character_classes.json", + "skill_categories.json", + "skill_difficulties.json", + "spell_schools.json", + "skills.json", + "skill_category_difficulties.json", + "spells.json", + "class_category_ep_costs.json", + "class_spell_school_ep_costs.json", + "spell_level_le_costs.json", + "skill_improvement_costs.json", + "weapon_skills.json", + "equipment.json", + "weapons.json", + "containers.json", + "transportation.json", + "believes.json", + } + for _, file := range files { + filename := filepath.Join(tempDir, file) + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Errorf("Export file not created: %s", filename) + } + } + + // Delete all test data + database.DB.Unscoped().Delete(&spell) + database.DB.Unscoped().Delete(&skill) + database.DB.Unscoped().Delete(&difficulty) + database.DB.Unscoped().Delete(&category) + // Don't delete source to avoid FK constraints + + // Import all + err = ImportAll(tempDir) + if err != nil { + t.Fatalf("ImportAll failed: %v", err) + } + + // Verify all data was recreated + var importedCategory models.SkillCategory + if err := database.DB.Where("name = ? AND game_system = ?", "AllCategory", "midgard").First(&importedCategory).Error; err != nil { + t.Errorf("Category not found after import: %v", err) + } + + var importedDifficulty models.SkillDifficulty + if err := database.DB.Where("name = ? AND game_system = ?", "AllDifficulty", "midgard").First(&importedDifficulty).Error; err != nil { + t.Errorf("Difficulty not found after import: %v", err) + } + + var importedSkill models.Skill + if err := database.DB.Where("name = ? AND game_system = ?", "AllSkill", "midgard").First(&importedSkill).Error; err != nil { + t.Errorf("Skill not found after import: %v", err) + } + + var importedSpell models.Spell + if err := database.DB.Where("name = ? AND game_system = ?", "AllSpell", "midgard").First(&importedSpell).Error; err != nil { + t.Errorf("Spell not found after import: %v", err) + } +} + +func TestExportAll_live(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB(false) + + // Export all + tempDir := t.TempDir() + err := ExportAll(tempDir) + if err != nil { + t.Fatalf("ExportAll failed: %v", err) + } + + // Verify all files were created + files := []string{"sources.json", "skill_categories.json", "skill_difficulties.json", "skills.json", "spells.json", "skill_category_difficulties.json"} + for _, file := range files { + filename := filepath.Join(tempDir, file) + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Errorf("Export file not created: %s", filename) + } + } + + assert.Equal(t, len(files), 6) + +} + +func TestExportImportWeaponSkills(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + source := getOrCreateSource("TEST_WS", "Test Weapon Source") + weaponSkill := models.WeaponSkill{ + Skill: models.Skill{ + Name: "Langschwert", + GameSystem: "midgard", + Beschreibung: "Langschwert Waffenfertigkeiten", + SourceID: source.ID, + PageNumber: 50, + Initialwert: 10, + BasisWert: 5, + Bonuseigenschaft: "St", + Improvable: true, + InnateSkill: false, + Category: "Waffen", + Difficulty: "normal", + }, + } + database.DB.Create(&weaponSkill) + + tempDir := t.TempDir() + err := ExportWeaponSkills(tempDir) + if err != nil { + t.Fatalf("ExportWeaponSkills failed: %v", err) + } + + filename := filepath.Join(tempDir, "weapon_skills.json") + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Fatalf("Export file not created: %s", filename) + } + + database.DB.Unscoped().Delete(&weaponSkill) + + err = ImportWeaponSkills(tempDir) + if err != nil { + t.Fatalf("ImportWeaponSkills failed: %v", err) + } + + var imported models.WeaponSkill + result := database.DB.Where("name = ? AND game_system = ?", "Langschwert", "midgard").First(&imported) + if result.Error != nil { + t.Fatalf("Weapon skill not found after import: %v", result.Error) + } + + assert.Equal(t, "Langschwert Waffenfertigkeiten", imported.Beschreibung) + assert.Equal(t, 10, imported.Initialwert) + assert.Equal(t, 5, imported.BasisWert) +} + +func TestExportImportEquipment(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + source := getOrCreateSource("TEST_EQ", "Test Equipment Source") + equipment := models.Equipment{ + Name: "Seil", + GameSystem: "midgard", + Beschreibung: "10m langes Hanfseil", + SourceID: source.ID, + PageNumber: 75, + Gewicht: 2.5, + Wert: 15.0, + PersonalItem: false, + } + database.DB.Create(&equipment) + + tempDir := t.TempDir() + err := ExportEquipment(tempDir) + if err != nil { + t.Fatalf("ExportEquipment failed: %v", err) + } + + filename := filepath.Join(tempDir, "equipment.json") + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Fatalf("Export file not created: %s", filename) + } + + equipment.Wert = 10.0 + database.DB.Save(&equipment) + + err = ImportEquipment(tempDir) + if err != nil { + t.Fatalf("ImportEquipment failed: %v", err) + } + + var imported models.Equipment + result := database.DB.Where("name = ? AND game_system = ?", "Seil", "midgard").First(&imported) + if result.Error != nil { + t.Fatalf("Equipment not found after import: %v", result.Error) + } + + assert.Equal(t, 15.0, imported.Wert) + assert.Equal(t, 2.5, imported.Gewicht) +} + +func TestExportImportWeapons(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + source := getOrCreateSource("TEST_WP", "Test Weapon Source") + weapon := models.Weapon{ + Equipment: models.Equipment{ + Name: "Kurzschwert", + GameSystem: "midgard", + Beschreibung: "Einhändiges Kurzschwert", + SourceID: source.ID, + PageNumber: 80, + Gewicht: 1.5, + Wert: 50.0, + PersonalItem: false, + }, + SkillRequired: "Langschwert", + Damage: "1W6+1", + RangeNear: 0, + RangeMiddle: 0, + RangeFar: 0, + } + database.DB.Create(&weapon) + + tempDir := t.TempDir() + err := ExportWeapons(tempDir) + if err != nil { + t.Fatalf("ExportWeapons failed: %v", err) + } + + filename := filepath.Join(tempDir, "weapons.json") + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Fatalf("Export file not created: %s", filename) + } + + weapon.Damage = "1W6" + database.DB.Save(&weapon) + + err = ImportWeapons(tempDir) + if err != nil { + t.Fatalf("ImportWeapons failed: %v", err) + } + + var imported models.Weapon + result := database.DB.Where("name = ? AND game_system = ?", "Kurzschwert", "midgard").First(&imported) + if result.Error != nil { + t.Fatalf("Weapon not found after import: %v", result.Error) + } + + assert.Equal(t, "1W6+1", imported.Damage) + assert.Equal(t, "Langschwert", imported.SkillRequired) +} + +func TestExportImportContainers(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + source := getOrCreateSource("TEST_CT", "Test Container Source") + container := models.Container{ + Equipment: models.Equipment{ + Name: "Rucksack", + GameSystem: "midgard", + Beschreibung: "Großer Lederrucksack", + SourceID: source.ID, + PageNumber: 85, + Gewicht: 1.0, + Wert: 20.0, + PersonalItem: false, + }, + Tragkraft: 30.0, + Volumen: 50.0, + } + database.DB.Create(&container) + + tempDir := t.TempDir() + err := ExportContainers(tempDir) + if err != nil { + t.Fatalf("ExportContainers failed: %v", err) + } + + filename := filepath.Join(tempDir, "containers.json") + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Fatalf("Export file not created: %s", filename) + } + + container.Tragkraft = 25.0 + database.DB.Save(&container) + + err = ImportContainers(tempDir) + if err != nil { + t.Fatalf("ImportContainers failed: %v", err) + } + + var imported models.Container + result := database.DB.Where("name = ? AND game_system = ?", "Rucksack", "midgard").First(&imported) + if result.Error != nil { + t.Fatalf("Container not found after import: %v", result.Error) + } + + assert.Equal(t, 30.0, imported.Tragkraft) + assert.Equal(t, 50.0, imported.Volumen) +} + +func TestExportImportTransportation(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + source := getOrCreateSource("TEST_TR", "Test Transport Source") + transportation := models.Transportation{ + Container: models.Container{ + Equipment: models.Equipment{ + Name: "Pferdewagen", + GameSystem: "midgard", + Beschreibung: "Zweirädriger Wagen", + SourceID: source.ID, + PageNumber: 90, + Gewicht: 100.0, + Wert: 200.0, + PersonalItem: false, + }, + Tragkraft: 500.0, + Volumen: 1000.0, + }, + } + database.DB.Create(&transportation) + + tempDir := t.TempDir() + err := ExportTransportation(tempDir) + if err != nil { + t.Fatalf("ExportTransportation failed: %v", err) + } + + filename := filepath.Join(tempDir, "transportation.json") + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Fatalf("Export file not created: %s", filename) + } + + transportation.Tragkraft = 450.0 + database.DB.Save(&transportation) + + err = ImportTransportation(tempDir) + if err != nil { + t.Fatalf("ImportTransportation failed: %v", err) + } + + var imported models.Transportation + result := database.DB.Where("name = ? AND game_system = ?", "Pferdewagen", "midgard").First(&imported) + if result.Error != nil { + t.Fatalf("Transportation not found after import: %v", result.Error) + } + + assert.Equal(t, 500.0, imported.Tragkraft) + assert.Equal(t, 1000.0, imported.Volumen) +} + +func TestExportImportBelieves(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + source := getOrCreateSource("TEST_BL", "Test Believe Source") + believe := models.Believe{ + Name: "Kirche des Lichts", + GameSystem: "midgard", + Beschreibung: "Hauptreligion in Valian", + SourceID: source.ID, + PageNumber: 95, + } + database.DB.Create(&believe) + + tempDir := t.TempDir() + err := ExportBelieves(tempDir) + if err != nil { + t.Fatalf("ExportBelieves failed: %v", err) + } + + filename := filepath.Join(tempDir, "believes.json") + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Fatalf("Export file not created: %s", filename) + } + + believe.Beschreibung = "Alte Beschreibung" + database.DB.Save(&believe) + + err = ImportBelieves(tempDir) + if err != nil { + t.Fatalf("ImportBelieves failed: %v", err) + } + + var imported models.Believe + result := database.DB.Where("name = ? AND game_system = ?", "Kirche des Lichts", "midgard").First(&imported) + if result.Error != nil { + t.Fatalf("Believe not found after import: %v", result.Error) + } + + assert.Equal(t, "Hauptreligion in Valian", imported.Beschreibung) + assert.Equal(t, 95, imported.PageNumber) +} + +func TestExportImportSkillImprovementCosts(t *testing.T) { + setupTestEnvironment(t) + database.SetupTestDB() + + // Create dependencies that already exist in test DB + // Use existing skill, category, difficulty from test database + var skill models.Skill + if err := database.DB.Where("name = ?", "Abrichten").First(&skill).Error; err != nil { + t.Skip("Test skill not found in database, skipping test") + } + + var category models.SkillCategory + if err := database.DB.First(&category).Error; err != nil { + t.Skip("No skill category found in database, skipping test") + } + + var difficulty models.SkillDifficulty + if err := database.DB.First(&difficulty).Error; err != nil { + t.Skip("No skill difficulty found in database, skipping test") + } + + // Find or create SkillCategoryDifficulty + var scd models.SkillCategoryDifficulty + err := database.DB.Where("skill_id = ? AND skill_category_id = ? AND skill_difficulty_id = ?", + skill.ID, category.ID, difficulty.ID).First(&scd).Error + + if err == gorm.ErrRecordNotFound { + scd = models.SkillCategoryDifficulty{ + SkillID: skill.ID, + SkillCategoryID: category.ID, + SkillDifficultyID: difficulty.ID, + LearnCost: 10, + SCategory: category.Name, + SDifficulty: difficulty.Name, + } + database.DB.Create(&scd) + } else if err != nil { + t.Fatalf("Failed to query SkillCategoryDifficulty: %v", err) + } + + // Create SkillImprovementCost + improvementCost := models.SkillImprovementCost{ + SkillCategoryDifficultyID: scd.ID, + CurrentLevel: 15, // Use unique level to avoid conflicts + TERequired: 5, + } + database.DB.Create(&improvementCost) + + // Export + tempDir := t.TempDir() + err = ExportSkillImprovementCosts(tempDir) + if err != nil { + t.Fatalf("ExportSkillImprovementCosts failed: %v", err) + } + + filename := filepath.Join(tempDir, "skill_improvement_costs.json") + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Fatalf("Export file not created: %s", filename) + } + + // Modify the record + improvementCost.TERequired = 7 + database.DB.Save(&improvementCost) + + // Import should restore original value + err = ImportSkillImprovementCosts(tempDir) + if err != nil { + t.Fatalf("ImportSkillImprovementCosts failed: %v", err) + } + + var imported models.SkillImprovementCost + result := database.DB.Where("skill_category_difficulty_id = ? AND current_level = ?", scd.ID, 15).First(&imported) + if result.Error != nil { + t.Fatalf("SkillImprovementCost not found after import: %v", result.Error) + } + + // Should be restored to original value from export + assert.Equal(t, 5, imported.TERequired) + assert.Equal(t, 15, imported.CurrentLevel) +} diff --git a/backend/models/model_gsmaster.go b/backend/models/model_gsmaster.go index 59a9473..ce53a96 100644 --- a/backend/models/model_gsmaster.go +++ b/backend/models/model_gsmaster.go @@ -128,51 +128,6 @@ type Believe struct { PageNumber int `json:"page_number,omitempty"` // Seitenzahl im Quellenbuch } -/* -func (object *LookupList) Create() error { - gameSystem := "midgard" - object.GameSystem = gameSystem - err := database.DB.Transaction(func(tx *gorm.DB) error { - // Save the main character record - if err := tx.Create(&object).Error; err != nil { - return fmt.Errorf("failed to save Lookup: %w", err) - } - return nil - }) - - return err -} - -func (object *LookupList) First(value string) error { - gameSystem := "midgard" - err := database.DB.First(&object, "game_system=? AND name!='Placeholder' AND name = ?", gameSystem, value).Error - if err != nil { - // zauber found - return err - } - return nil -} - -func (object *LookupList) FirstId(value uint) error { - gameSystem := "midgard" - err := database.DB.First(&object, "game_system=? AND name!='Placeholder' AND id = ?", gameSystem, value).Error - if err != nil { - // zauber found - return err - } - return nil -} - -func (object *LookupList) Save() error { - err := database.DB.Save(&object).Error - if err != nil { - // zauber found - return err - } - return nil -} -*/ - func (object *Skill) TableName() string { dbPrefix := "gsm" return dbPrefix + "_" + "skills"