noeva-etl — Technical Overview
Documento condivisibile. Spiega cos'è, per cosa è pensato, cosa può fare, come si espande e qual è la direzione del prodotto. Versione: prodotto
0.1.x.
1. In una frase
noeva-etl è una piattaforma BI/ETL code-first in TypeScript: si costruiscono pipeline di integrazione dati (estrazione → trasformazione → caricamento) come software versionato, testato e documentato, non come job grafici opachi. Il nucleo di esecuzione di ogni pipeline è semplice, stateless e portabile: esegue una pipeline e restituisce un risultato completo (report di run), senza possedere database né stato proprio.
Il servizio si invoca in tre modi (vedi §5): batch (etl run, un container = una esecuzione, esito su stdout), daemon (etl worker, poller sulla tabella processes di Noeva, per l'integrazione con l'orchestrazione job del prodotto) e server + daemon (etl serve, il default dell'immagine: espone i grafi e le run all'app desktop noeva-etl-ui e avvia i worker nello stesso processo).
2. Per cosa è stato pensato
Sostituire l'approccio "ETL a scatola nera" (es. Talend) con uno code-first:
- Versionabile — ogni pipeline è codice in git, rivedibile in Pull Request.
- Testabile — connettori, trasformazioni e pipeline hanno test automatici (gate in CI).
- Documentato — ogni pipeline/decisione ha la sua scheda/ADR, aggiornati nello stesso commit.
- Portabile — un'unica immagine container gira sull'infra del cliente (Docker locale, ECS proprio o gestito da noi).
- Agnostico — nessun lock-in: sorgenti e destinazioni sono intercambiabili; il servizio non impone un DB o un cloud.
Target d'uso: integrazioni dati ricorrenti e affidabili (sincronizzazioni, fatti per la BI, normalizzazioni cross-database) dove correttezza, ripetibilità e tracciabilità contano.
3. Cosa può fare oggi
| Capacità | Dettaglio |
|---|---|
| Primitive componibili | Source<T> (estrazione), Transform<I,O> (trasformazione), Sink<T> (caricamento), GraphPipeline (DSL a grafo graph()...build()) |
| Nodi del grafo | source, transform, lookup (query SQL in sola lettura a metà grafo, ADR-0041), join (unico fan-in), try-catch (ramo di gestione errori), sink |
| Streaming | I connettori lavorano su AsyncIterable: dataset grandi senza caricarli in memoria |
| Contratti dati (Zod) | Validazione di schema su input e output di ogni pipeline (validate) |
| Trasformazioni | map, filter, e aggregazione stateful riusabile groupBy (init/reduce/finalize) |
| Gestione errori | dead-letter per nodo (record invalidi instradati altrove, la run prosegue), ramo catch di un nodo try-catch, o fail-fast esplicito — mai scarti silenziosi |
| Idempotenza | Sink con upsert/MERGE su chiave naturale: rieseguire non duplica |
| Connettori reali | PostgreSQL (cursor streaming + upsert ON CONFLICT + lookup), SQL Server (streaming + MERGE + lookup), file (filesystem/S3/SFTP/FTP × CSV/JSON/XML/XLSX/Parquet), Noeva (file e cartelle dei workspace) |
| Cross-database | Una pipeline può leggere da un DB e scrivere su un altro (es. PG → SQL Server) |
| GraphRunReport | Risultato auto-descrittivo di ogni run (vedi §6) |
| CLI & container | etl list / etl run <pipeline> / etl serve / etl runs list; immagine Docker multi-arch su ECR |
| App desktop | noeva-etl-ui (Tauri): DAG delle pipeline, avvio manuale, storico ed esito delle run, nodo per nodo — mostra, non costruisce |
| Worker daemon | etl worker — poller su processes via noeva-server-api (claim/progress HTTP, mai Supabase diretto), dispatch verso il registry pipeline; una istanza serve N workspace (ADR-0021) |
Esempio reale incluso: langfuse-sessions-daily legge le sessioni da PostgreSQL, le aggrega per progetto/giorno e carica i fatti su SQL Server (cross-DB, idempotente).
4. Come funziona (architettura)
Ogni pipeline è un grafo di nodi (DAG), costruito in codice col DSL graph()…build():
flowchart LR
ordini["source<br/>ordini"] --> valida["transform<br/>validate (contratto)"]
clienti["source<br/>clienti"] --> join["join<br/>(a chiave)"]
valida -.record invalido.-> scarti["dead-letter"]
valida --> join
join --> lookup["lookup<br/>(query SQL read-only)"]
lookup --> guard["try-catch"]
guard --> dwh["sink<br/>DWH"]
guard --> file["sink<br/>file"]
guard -.catch.-> errori["ramo errori"]
- Core (
src/core) — primitive + runner; non contiene logica di dominio. - Dependency Injection — la definizione della pipeline è agnostica: sorgente e sink concreti si iniettano all'avvio. Per questo lo stesso codice gira su DB diversi e si testa con connettori in-memory.
- Streaming pull-based — i record scorrono dai source ai sink senza essere caricati tutti in memoria; è il ritmo di scrittura a regolare quello di lettura.
- Tre percorsi per un record che fallisce: dead-letter sul nodo (il record va altrove, la run prosegue), ramo catch di un nodo
try-catch(gli errori di un intero tratto del grafo finiscono su nodi di gestione dedicati), fail-fast (la run si ferma — obbligatorio per le aggregazioni stateful). - Osservabilità — logger JSON strutturato + metriche; ogni run produce un
GraphRunReportcon i conteggi di ogni nodo.
Stack: TypeScript ESM strict, Node ≥ 20, pnpm, tsup, vitest, ESLint/Prettier, Zod. Dipendenze runtime: i driver dei database (pg, mssql), le librerie per storage e formati dei file (AWS SDK, SFTP, FTP, CSV, XLSX, XML, Parquet), express per il server locale di etl serve — ciascuna introdotta con un ADR. Nessuna dipendenza Supabase: il claim/report sulla tabella processes passa da noeva-server-api via HTTP (fetch nativo).
5. Come si usa e si distribuisce
# in sviluppo
pnpm run etl list
pnpm run etl run langfuse-sessions-daily
# build & push immagine multi-arch su ECR (latest + tag temporale)
./deploy-ecr.sh
# pull da ECR ed esegui in locale (Docker Desktop)
./run-ecr.sh langfuse-sessions-daily
# server + daemon: grafi e run via HTTP per l'app desktop, più i worker
pnpm run etl:serve
# solo daemon: polling continuo su processes (worker_type da env; uno o più
# workspace — NOEVA_WORKSPACE_IDS per il multi-workspace, ADR-0021)
pnpm run etl worker- Configurazione solo da variabili d'ambiente (
PG*,MSSQL_*,ETL_ENV, …); nessun segreto nel codice o nell'immagine. - Tre modalità di invocazione: batch (
etl run, un container = una esecuzione, poi termina — vedi ADR-0005 e runbooks/deployment.md); daemon (etl worker, resta in polling suprocessesfinché non riceveSIGTERM/SIGINT— vedi ADR-0019 e runbooks/etl-worker.md); server + daemon (etl serve, ilCMDdi default dell'immagine: un solo container per tutta la piattaforma — vedi ADR-0028 e runbooks/graph-server.md). - App desktop
noeva-etl-ui: si distribuisce come applicazione Tauri, non da ECR (ADR-0025). - Modello di deploy (attuale): immagine su ECR, scaricata ed eseguita in locale su Docker Desktop. È pronta anche per ECS (nostro o del cliente) quando servirà.
6. Il report di run è il "prodotto"
Il nucleo di esecuzione non persiste nulla: esegue e restituisce un risultato completo. È il chiamante a conservarlo: la CLI tiene le ultime run in locale (etl runs list, ADR-0039), il worker lo scrive nel job Noeva.
stdout= report in JSON (un oggetto, parsabile conjq);stderr= log.- exit code:
0sesuccess/partial,≠0sefailed. - Anche in errore emette un report (
status: failed) con dettagli e conteggi parziali. - Contiene: stato, tempi, versione, topologia reale del grafo (nodi e archi, con i conteggi e i tempi di ogni nodo), parametri, volumi (
extracted/written/rejected), categorie di scarto, metriche, errore — senza segreti né payload/PII.
Chi invoca decide cosa farne (archiviarlo, mandarlo in alert, ignorarlo). Vedi run-report.md.
7. Come si espande
Crescita per composizione, senza toccare il core. Comandi di scaffold coerenti generano struttura + test + doc:
- Nuova pipeline →
/new-pipeline <nome>(schema, definizione, run, test, scheda doc). - Nuovo connettore →
/new-connector <nome> [source|sink](verso un nuovo sistema: API, file/CSV, altri DB, code…). - Nuova trasformazione →
/new-transform <nome>(logica riusabile, pura e testabile).
Aggiungere un sistema = scrivere un Source/Sink che rispetta l'interfaccia; tutte le pipeline esistenti possono usarlo subito (DI). La logica di dominio vive nelle trasformazioni, riusabili tra pipeline.
8. Perché ci si può fidare (governance)
- Test obbligatori con soglie di coverage come gate di CI (GitHub Actions).
- Documentazione nello stesso commit del codice (schede pipeline, ADR, runbook).
- Decisioni tracciate in un registro di ADR (es. scelta code-first, connettori, distribuzione, report di run, worker daemon, modello a grafo, gestione errori).
- Assistenti di qualità versionati (
.claude/agents/): review ETL, audit dei contratti, sync della doc, autore dei test. - Regole operative vincolanti in CLAUDE.md.
9. Direzione del prodotto (chiara e semplice)
Un motore ETL piccolo, code-first e agnostico, che esegue pipeline affidabili e restituisce un risultato completo — e cresce per connettori e pipeline, non per complessità.
Principi guida (stabili):
- Semplice e stateless — il servizio è una funzione
pipeline + config → report. - Agnostico — nessun DB interno, nessun cloud obbligato, nessun push integrato.
- Code-first — niente builder grafici; le pipeline restano codice testato e documentato.
- Portabile — un'immagine, eseguibile ovunque, configurata da env.
- Espandibile per composizione — nuovi connettori/trasformazioni/pipeline, core stabile.
Cosa NON è (non-obiettivi, per scelta)
- ❌ Non è un pipeline builder grafico: l'app desktop
noeva-etl-uimostra i DAG e le run, ma le pipeline si scrivono in codice. - ❌ Non è uno strumento di visualizzazione BI (per i grafici si collega Metabase/Grafana/Power BI alle tabelle prodotte).
- ❌ Non possiede un database di stato (storico/audit = archiviazione dei report, a cura del chiamante).
Evoluzioni possibili (senza tradire i principi)
- Scheduling esterno (cron / ECS Scheduled Task) — l'idempotenza rende sicure retry e sovrapposizioni.
- Nuovi connettori (API REST, data warehouse, code di messaggi) e nuovi resolver per il nodo
lookup(oggi PostgreSQL e SQL Server; un nuovo DBMS riusaconnectors/shared/sql-lookup.ts). - Orchestrazione per dipendenze/backfill complessi (es. Temporal TS), solo se e quando servirà.
- Persistenza opzionale dei report come adapter lato chiamante, mai dentro il servizio.
Riferimenti: README · architettura · ADR · connettori · schede pipeline · report di run · deployment · etl worker · etl serve.

