SOGOMS v1.0.1 - Microservices logs, smtp et roadmap
Nouveaux services:
- sogoms-logs : logging centralisé avec rotation
- sogoms-smtp : envoi emails avec templates YAML
Nouvelles fonctionnalités:
- Queries YAML externalisées (config/queries/{app}/)
- CRUD générique paramétrable
- Filtres par rôle (default, admin)
- Templates email (config/emails/{app}/)
Documentation:
- DOCTECH.md : documentation technique complète
- README.md : vision et roadmap
- TODO.md : phases 11-15 planifiées
Roadmap:
- Phase 11: sogoms-crypt (chiffrement)
- Phase 12: sogoms-imap/mailproc (emails)
- Phase 13: sogoms-cron (tâches planifiées)
- Phase 14: sogoms-push (MQTT temps réel)
- Phase 15: sogoms-schema (API auto-générée)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,227 @@ type AppConfig struct {
|
||||
Database Database `yaml:"database"`
|
||||
Auth Auth `yaml:"auth"`
|
||||
Routes []Route `yaml:"routes"`
|
||||
Queries *Queries // Chargé depuis config/queries/{app}/
|
||||
}
|
||||
|
||||
// Queries stocke les requêtes SQL par domaine.
|
||||
type Queries struct {
|
||||
Auth map[string]any `yaml:",inline"`
|
||||
Projects map[string]any `yaml:",inline"`
|
||||
Tasks map[string]any `yaml:",inline"`
|
||||
Tags map[string]any `yaml:",inline"`
|
||||
Statuses map[string]any `yaml:",inline"`
|
||||
files map[string]map[string]any // domaine -> clé -> requête
|
||||
}
|
||||
|
||||
// Get retourne une requête par domaine et clé.
|
||||
func (q *Queries) Get(domain, key string) string {
|
||||
if q == nil || q.files == nil {
|
||||
return ""
|
||||
}
|
||||
if domainMap, ok := q.files[domain]; ok {
|
||||
if val, ok := domainMap[key]; ok {
|
||||
if s, ok := val.(string); ok {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetMap retourne une map de requêtes (ex: login_data).
|
||||
func (q *Queries) GetMap(domain, key string) map[string]string {
|
||||
if q == nil || q.files == nil {
|
||||
return nil
|
||||
}
|
||||
if domainMap, ok := q.files[domain]; ok {
|
||||
if val, ok := domainMap[key]; ok {
|
||||
if m, ok := val.(map[string]any); ok {
|
||||
result := make(map[string]string)
|
||||
for k, v := range m {
|
||||
if s, ok := v.(string); ok {
|
||||
result[k] = strings.TrimSpace(s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueryConfig représente une requête paramétrable.
|
||||
type QueryConfig struct {
|
||||
Query string
|
||||
Filters map[string]string
|
||||
Order string
|
||||
}
|
||||
|
||||
// GetQuery retourne une QueryConfig pour un domaine et une clé.
|
||||
func (q *Queries) GetQuery(domain, key string) *QueryConfig {
|
||||
if q == nil || q.files == nil {
|
||||
return nil
|
||||
}
|
||||
domainMap, ok := q.files[domain]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
val, ok := domainMap[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
m, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
// Ancienne syntaxe : juste une string
|
||||
if s, ok := val.(string); ok {
|
||||
return &QueryConfig{Query: strings.TrimSpace(s)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
qc := &QueryConfig{
|
||||
Filters: make(map[string]string),
|
||||
}
|
||||
|
||||
if query, ok := m["query"].(string); ok {
|
||||
qc.Query = strings.TrimSpace(query)
|
||||
}
|
||||
if order, ok := m["order"].(string); ok {
|
||||
qc.Order = strings.TrimSpace(order)
|
||||
}
|
||||
if filters, ok := m["filters"].(map[string]any); ok {
|
||||
for k, v := range filters {
|
||||
if s, ok := v.(string); ok {
|
||||
qc.Filters[k] = strings.TrimSpace(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return qc
|
||||
}
|
||||
|
||||
// CUDConfig représente une config pour Create/Update/Delete.
|
||||
type CUDConfig struct {
|
||||
Table string
|
||||
Fields []string
|
||||
Filters map[string]string
|
||||
}
|
||||
|
||||
// GetCUD retourne une CUDConfig pour un domaine et une clé (create/update/delete).
|
||||
func (q *Queries) GetCUD(domain, key string) *CUDConfig {
|
||||
if q == nil || q.files == nil {
|
||||
return nil
|
||||
}
|
||||
domainMap, ok := q.files[domain]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
val, ok := domainMap[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
m, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
cud := &CUDConfig{
|
||||
Filters: make(map[string]string),
|
||||
}
|
||||
|
||||
if table, ok := m["table"].(string); ok {
|
||||
cud.Table = table
|
||||
}
|
||||
if fields, ok := m["fields"].([]any); ok {
|
||||
for _, f := range fields {
|
||||
if s, ok := f.(string); ok {
|
||||
cud.Fields = append(cud.Fields, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if filters, ok := m["filters"].(map[string]any); ok {
|
||||
for k, v := range filters {
|
||||
if s, ok := v.(string); ok {
|
||||
cud.Filters[k] = strings.TrimSpace(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cud
|
||||
}
|
||||
|
||||
// GetFilter retourne le filtre pour un rôle donné.
|
||||
func (cud *CUDConfig) GetFilter(role string) string {
|
||||
if cud == nil {
|
||||
return ""
|
||||
}
|
||||
if f, ok := cud.Filters[role]; ok {
|
||||
return f
|
||||
}
|
||||
if f, ok := cud.Filters["default"]; ok {
|
||||
return f
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Build construit la requête SQL finale avec filtres et ordre.
|
||||
// role: rôle de l'utilisateur (ou "default")
|
||||
// params: map de placeholders à remplacer (:user_id, :id, etc.)
|
||||
func (qc *QueryConfig) Build(role string, params map[string]any) (string, []any) {
|
||||
if qc == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
query := qc.Query
|
||||
|
||||
// Déterminer le filtre à appliquer
|
||||
filter := ""
|
||||
if f, ok := qc.Filters[role]; ok {
|
||||
filter = f
|
||||
} else if f, ok := qc.Filters["default"]; ok {
|
||||
filter = f
|
||||
}
|
||||
|
||||
// Construire la requête
|
||||
hasWhere := strings.Contains(strings.ToUpper(query), " WHERE ")
|
||||
|
||||
if filter != "" {
|
||||
if hasWhere {
|
||||
query += " AND " + filter
|
||||
} else {
|
||||
query += " WHERE " + filter
|
||||
}
|
||||
}
|
||||
|
||||
if qc.Order != "" {
|
||||
query += " ORDER BY " + qc.Order
|
||||
}
|
||||
|
||||
// Remplacer les placeholders :name par ? et collecter les args
|
||||
var args []any
|
||||
for {
|
||||
idx := strings.Index(query, ":")
|
||||
if idx == -1 {
|
||||
break
|
||||
}
|
||||
// Trouver la fin du placeholder
|
||||
end := idx + 1
|
||||
for end < len(query) && (query[end] == '_' || (query[end] >= 'a' && query[end] <= 'z') || (query[end] >= 'A' && query[end] <= 'Z') || (query[end] >= '0' && query[end] <= '9')) {
|
||||
end++
|
||||
}
|
||||
placeholder := query[idx+1 : end]
|
||||
if val, ok := params[placeholder]; ok {
|
||||
args = append(args, val)
|
||||
query = query[:idx] + "?" + query[end:]
|
||||
} else {
|
||||
// Placeholder non trouvé, on laisse tel quel (peut être une erreur)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return query, args
|
||||
}
|
||||
|
||||
// Auth contient la configuration d'authentification.
|
||||
@@ -151,9 +372,48 @@ func (r *Registry) loadAppConfig(path string) (*AppConfig, error) {
|
||||
cfg.Auth.JWTExpiry = "24h"
|
||||
}
|
||||
|
||||
// Charger les requêtes depuis config/queries/{app}/
|
||||
cfg.Queries = r.loadQueries(cfg.App)
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// loadQueries charge les fichiers de requêtes pour une application.
|
||||
func (r *Registry) loadQueries(appID string) *Queries {
|
||||
queriesDir := filepath.Join(r.configDir, "queries", appID)
|
||||
entries, err := os.ReadDir(queriesDir)
|
||||
if err != nil {
|
||||
return nil // Pas de répertoire queries, c'est OK
|
||||
}
|
||||
|
||||
q := &Queries{
|
||||
files: make(map[string]map[string]any),
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
|
||||
continue
|
||||
}
|
||||
|
||||
domain := strings.TrimSuffix(entry.Name(), ".yaml")
|
||||
path := filepath.Join(queriesDir, entry.Name())
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var content map[string]any
|
||||
if err := yaml.Unmarshal(data, &content); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
q.files[domain] = content
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
// GetByApp retourne la configuration d'une application par son ID.
|
||||
func (r *Registry) GetByApp(appID string) (*AppConfig, bool) {
|
||||
r.mu.RLock()
|
||||
|
||||
Reference in New Issue
Block a user