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:
2025-12-16 14:58:46 +01:00
parent 7e27f87d6f
commit a4694a10d1
36 changed files with 2786 additions and 2387 deletions

View File

@@ -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()