// Package config - Schema représente la structure de la base de données. package config import ( "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) // Schema représente le schema d'une application. type Schema struct { App string `yaml:"app"` Version string `yaml:"version"` Tables map[string]*Table `yaml:"tables"` } // Table représente une table de la base de données. type Table struct { Columns map[string]*Column `yaml:"columns"` Primary []string `yaml:"primary,omitempty"` // Clé primaire composite CRUD []string `yaml:"crud"` Order string `yaml:"order,omitempty"` } // Column représente une colonne d'une table. type Column struct { Type string `yaml:"type"` Length int64 `yaml:"length,omitempty"` Required bool `yaml:"required,omitempty"` Primary bool `yaml:"primary,omitempty"` Auto bool `yaml:"auto,omitempty"` Unique bool `yaml:"unique,omitempty"` Default string `yaml:"default,omitempty"` Foreign string `yaml:"foreign,omitempty"` // table.column Filter string `yaml:"filter,omitempty"` // "owner" pour filtrage auto } // HasCRUD vérifie si une opération CRUD est autorisée pour cette table. func (t *Table) HasCRUD(op string) bool { for _, c := range t.CRUD { if c == op { return true } } return false } // GetOwnerColumn retourne le nom de la colonne avec filter: owner, ou "". func (t *Table) GetOwnerColumn() string { for name, col := range t.Columns { if col.Filter == "owner" { return name } } return "" } // GetPrimaryKey retourne le nom de la clé primaire (simple). func (t *Table) GetPrimaryKey() string { // D'abord chercher dans Primary (composite) if len(t.Primary) == 1 { return t.Primary[0] } // Sinon chercher une colonne avec primary: true for name, col := range t.Columns { if col.Primary { return name } } return "id" // Par défaut } // IsCompositePK vérifie si la table a une clé primaire composite. func (t *Table) IsCompositePK() bool { return len(t.Primary) > 1 } // GetSelectColumns retourne la liste des colonnes pour un SELECT. func (t *Table) GetSelectColumns() []string { cols := make([]string, 0, len(t.Columns)) for name := range t.Columns { cols = append(cols, name) } return cols } // GetInsertColumns retourne les colonnes pour un INSERT (exclut auto-increment). func (t *Table) GetInsertColumns() []string { cols := make([]string, 0, len(t.Columns)) for name, col := range t.Columns { if !col.Auto { cols = append(cols, name) } } return cols } // GetUpdateColumns retourne les colonnes pour un UPDATE (exclut PK et auto). func (t *Table) GetUpdateColumns() []string { cols := make([]string, 0, len(t.Columns)) pk := t.GetPrimaryKey() for name, col := range t.Columns { if name != pk && !col.Auto && col.Filter != "owner" { cols = append(cols, name) } } return cols } // BuildListQuery génère la requête SELECT pour list. func (t *Table) BuildListQuery(tableName string) string { cols := t.GetSelectColumns() query := fmt.Sprintf("SELECT %s FROM %s", strings.Join(cols, ", "), tableName) // Ajouter filtre owner si présent if ownerCol := t.GetOwnerColumn(); ownerCol != "" { query += fmt.Sprintf(" WHERE %s = ?", ownerCol) } // Ajouter ORDER BY si défini if t.Order != "" { query += " ORDER BY " + t.Order } return query } // BuildShowQuery génère la requête SELECT pour show (un seul enregistrement). func (t *Table) BuildShowQuery(tableName string) string { cols := t.GetSelectColumns() pk := t.GetPrimaryKey() query := fmt.Sprintf("SELECT %s FROM %s WHERE %s = ?", strings.Join(cols, ", "), tableName, pk) // Ajouter filtre owner si présent if ownerCol := t.GetOwnerColumn(); ownerCol != "" { query += fmt.Sprintf(" AND %s = ?", ownerCol) } return query } // BuildInsertQuery génère la requête INSERT. func (t *Table) BuildInsertQuery(tableName string, data map[string]any) (string, []any) { cols := make([]string, 0) placeholders := make([]string, 0) args := make([]any, 0) for name, col := range t.Columns { if col.Auto { continue // Skip auto-increment } if val, ok := data[name]; ok { cols = append(cols, name) placeholders = append(placeholders, "?") args = append(args, val) } else if col.Required && col.Default == "" { // Champ requis sans valeur et sans default - sera une erreur DB continue } } query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", tableName, strings.Join(cols, ", "), strings.Join(placeholders, ", ")) return query, args } // BuildUpdateQuery génère la requête UPDATE. func (t *Table) BuildUpdateQuery(tableName string, id any, ownerID any, data map[string]any) (string, []any) { setClauses := make([]string, 0) args := make([]any, 0) pk := t.GetPrimaryKey() ownerCol := t.GetOwnerColumn() for name := range t.Columns { // Skip PK, auto, et owner if name == pk || t.Columns[name].Auto || name == ownerCol { continue } if val, ok := data[name]; ok { setClauses = append(setClauses, name+" = ?") args = append(args, val) } } if len(setClauses) == 0 { return "", nil } query := fmt.Sprintf("UPDATE %s SET %s WHERE %s = ?", tableName, strings.Join(setClauses, ", "), pk) args = append(args, id) // Ajouter filtre owner if ownerCol != "" && ownerID != nil { query += fmt.Sprintf(" AND %s = ?", ownerCol) args = append(args, ownerID) } return query, args } // BuildDeleteQuery génère la requête DELETE. func (t *Table) BuildDeleteQuery(tableName string, id any, ownerID any) (string, []any) { pk := t.GetPrimaryKey() query := fmt.Sprintf("DELETE FROM %s WHERE %s = ?", tableName, pk) args := []any{id} // Ajouter filtre owner if ownerCol := t.GetOwnerColumn(); ownerCol != "" && ownerID != nil { query += fmt.Sprintf(" AND %s = ?", ownerCol) args = append(args, ownerID) } return query, args } // ValidateInput valide les données d'entrée selon le schema. // Retourne les données filtrées (seuls les champs connus sont gardés). func (t *Table) ValidateInput(data map[string]any) map[string]any { filtered := make(map[string]any) for name := range t.Columns { if val, ok := data[name]; ok { filtered[name] = val } } return filtered } // loadSchema charge le schema depuis config/apps/{app}/schema.yaml. func loadSchema(configDir, appID string) *Schema { schemaPath := filepath.Join(configDir, "apps", appID, "schema.yaml") data, err := os.ReadFile(schemaPath) if err != nil { return nil // Pas de schema, c'est OK } var schema Schema if err := yaml.Unmarshal(data, &schema); err != nil { return nil } return &schema }