Skip to content

Descriptor dei nodi per la UI del DAG

Come "dettagliare" un nodo del DAG affinché noeva-etl-ui mostri che connettore è e dove punta, senza mai esporre segreti. Decisione e razionale: ADR-0020. Campo gemello dello stesso PlanNode: le colonne del dato — quali campi scorrono nel nodo — vivono in columns, vedi node-columns.md (ADR-0026).

Il contratto

Ogni nodo source/sink/lookup può esporre un ConnectorDescriptor (src/core/connector.ts):

ts
interface ConnectorDetail {
  readonly label: string; // es. "host", "database", "table", "query"
  readonly value: string;
  readonly sensitive?: boolean; // hint di rendering: la UI mostra "••••"
}

interface ConnectorConnectionRef {
  readonly id: string; // nome logico dell'istanza nel registry (ADR-0030), es. "dwh"
  readonly name?: string; // etichetta leggibile; la UI mostra name ?? id
}

interface ConnectorDescriptor {
  readonly kind: string; // es. "postgres", "sqlserver", "noeva-file", "file", "memory"
  readonly connection?: ConnectorConnectionRef; // assente senza registry
  readonly details: readonly ConnectorDetail[];
}

sensitive è solo un suggerimento per la UI, non una protezione: un detail marcato sensitive porta al più un valore già parziale calcolato dall'engine (es. ••••ABCD), mai il segreto intero. La garanzia resta la regola 2 qui sotto. Oggi nessun connettore lo usa.

Il campo è opzionale su Source/Sink (descriptor?). toPlanSpec (src/core/graph.ts) lo proietta su PlanNode.descriptor, da cui viaggia in etl planplans.generated.json → UI.

Solo i nodi che parlano con l'esterno: source, sink e lookup. Il lookup prende il descriptor dal suo LookupResolver (mssqlLookup: kind sqlserver-lookup; postgresLookup: kind postgres-lookup; entrambi con la query). transform/join/try-catch non sono connettori e non ricevono un descriptor (planNodeDescriptor ritorna undefined per quei kind).

Le tre regole vincolanti

  1. Whitelist esplicita, mai un dump. details elenca campi scelti a mano. Non serializzare l'oggetto opzioni: conterrebbe password/apiKey/pool.
  2. Niente segreti. host/port/database si leggono dalla config pubblica del pool, mai da campi credenziale. Se un valore non è derivabile (es. host senza pool live), si omette il detail — non si inventa e non si mette undefined.
  3. Un test anti-segreto per connettore. Ogni connettore ha un descriptor.test.ts che verifica kind, i campi attesi e che nessun value contenga il segreto della fixture (vedi esempi in src/connectors/*/).

Campi esposti oggi, per connettore

Connettorekinddetails (label) — solo non sensibiliFonte
Postgrespostgreshost, port, database, query (source) / table (sink)pool.options + query/table
SQL Serversqlserverhost, port, database, query (source) / table (sink)pool.config (runtime) + query/table
Noeva filenoeva-filebaseUrl, workspace, fileName/fileId/fuzzyQuery, folderIdgetter pubblici del client + opzioni
Filefilestorage, codec, path/prefixnome storage/codec + opzioni
Noeva cartellanoeva-folderbaseUrl, workspace, folderId, includeSubfolders, skipIfMetadataKey, maxFilesgetter pubblici del client + opzioni
Noeva multi-filenoeva-filesbaseUrl, workspace, mode, folderIdgetter pubblici del client + opzioni
SQL Server lookupsqlserver-lookuphost, port, database, query (+ connection dal registry)pool.config (runtime) + query
Postgres lookuppostgres-lookuphost, port, database, query (+ connection dal registry)pool.options + query
Array (mem.)memory— (nessuno)arraySource/arraySink

La query è mostrata troncata a 120 caratteri (queryDetail): è un "puntamento", non il payload. host/database restano esclusi quando non c'è un pool attivo (caso getSpec() statico, vedi sotto).

Aggiungere un descriptor a un nuovo connettore

  1. Nel factory del Source/Sink, costruisci il descriptor con i soli campi non sensibili noti alla costruzione:

    ts
    return {
      name,
      descriptor: {
        kind: "mio-sistema",
        details: [
          { label: "endpoint", value: options.endpoint }, // non sensibile
          // NON: { label: "token", value: options.token }  ← mai
        ],
      },
      async *extract(ctx) {
        /* ... */
      },
    };
  2. Se i campi vengono da un client/pool con credenziali, esponi getter pubblici solo per i valori non sensibili (vedi NoevaHttpClient.baseUrl/workspaceId) e un helper dedicato (src/connectors/<sistema>/descriptor.ts).

  3. Scrivi il test anti-segreto colocato (descriptor.test.ts).

  4. Aggiorna la riga di questo file e la scheda del connettore in docs/connectors/.

getSpec() senza credenziali: connettori stub

etl plan non apre connessioni, quindi le pipeline con connettori a DB o API derivano il piano dal grafo reale costruito con connettori stub (vedi crm-leads-sync/run.ts, langfuse-sessions-daily/run.ts). Il descriptor sta sullo stub, coi soli campi noti senza connessione (kind, query, tabella); host/database si omettono. Riusa queryDetail dal barrel del connettore per troncare la query in modo coerente con il connettore vero. Non si scrive un PlanSpec letterale a mano (ETL-5).

Nodi dead-letter

toPlanSpec proietta il deadLetter di un nodo transform/join in un nodo sintetico stepType: "dead-letter" + edge kind: "dead-letter" (ADR-0027). Quel nodo riusa il descriptor del sink di scarto per dire dove finiscono gli errori (kind + destinazione), esattamente come un sink normale — nessuna regola nuova qui, solo un altro stepType che porta lo stesso descriptor?.

Sync con la UI

Il contratto è duplicato in noeva-etl-ui (submodule indipendente): tipo ConnectorDescriptor, schema Zod in plan-loader.ts, rendering in DatasetNode/DetailPanel. Modifiche al descriptor vanno propagate su entrambi i lati nello stesso PR-set. Lato UI: noeva-etl-ui/docs/dag-node-visualization.md.

Noeva è un marchio registrato di 4D S.R.L.