Tema
Nodo: lookup
Arricchisce ogni record leggendo da un sistema esterno — un database o un'API REST — in sola lettura e a batch di chiavi. La query è configurazione del nodo, non una closure: l'accesso al sistema esterno resta visibile nel DAG e in review.
| Metodo | .lookup(from, id, config, { deadLetter?, fromPort?, purpose?, recordReport?, writeReport? }) |
|---|---|
| Ingressi | 1 |
| Uscite | ≥1 (fan-out libero) |
| Dead-letter | sì, per record |
| Codice | src/core/lookup.ts; resolver nei connettori |
Quando usarlo
- Il lato di destra è grande e serve solo per le chiavi presenti nel flusso.
- Un record porta più chiavi candidate, da provare in ordine.
- I dati stanno dietro un'API, non in un flusso che si possa leggere per intero.
Se il lato di destra è piccolo e si legge tutto, un join fra due source è più semplice. Se l'operazione scrive, è un'action; se la risposta la dà un modello — noevaAiExtract — è un nodo ai, che ha la stessa configurazione ma lo dichiara nel DAG.
La configurazione
| Campo | Default | Effetto |
|---|---|---|
resolver | — | Il LookupResolver: risolve un batch di chiavi in un round-trip. Porta il descriptor del DAG |
key | — | (record) => chiave o un array di chiavi; null, undefined, "" o [] = nessuna query |
merge | — | (rows, record) => output: combina le righe trovate col record |
onNotFound | "null" | "null": il record passa con zero righe, come un left join. "error": LookupError |
batchSize | 200 | Chiavi per round-trip |
cache | true | Tiene i risultati per la durata della run |
maxCacheEntries | 50000 | Oltre, la cache smette di crescere |
outputSchema | — | Colonne del nodo nel DAG (non valida i record) |
retry | — | { attempts, baseMs?, maxMs?, jitter? }: ritenta il round-trip sugli errori transitori |
rateLimit | — | { perSecond }: distanzia i round-trip |
Con più chiavi per record, le righe arrivano concatenate nell'ordine delle chiavi: a valle si può scegliere per rango.
Resolver pronti
| Resolver | Sistema | Scheda |
|---|---|---|
postgresLookup | PostgreSQL | postgres § Lookup |
mssqlLookup | SQL Server | sqlserver § Lookup |
mysqlLookup | MySQL, MariaDB | mysql § Lookup |
httpLookup | API REST (batch o per-key) | http |
jiraIssueLookup | Jira Cloud (issue per chiave) | jira |
jiraChangelogLookup, jiraCommentsLookup, jiraWorklogsLookup | Jira Cloud (liste di una issue) | jira |
noevaElaborazioneLookup, noevaContenutoLookup | Stato e testo di un file Noeva | noeva |
I resolver SQL condividono la stessa query, col segnaposto @chiavi, e rifiutano a build() tutto ciò che non sia una singola SELECT/WITH. Con keysAs: "rows" le chiavi entrano come tabella (FROM @chiavi AS k(chiave) JOIN …), per i confronti che non sono un'uguaglianza.
Esempi
Da un database:
ts
.lookup<Riga, RigaConCliente>("righe", "cliente", {
resolver: mssqlLookup({
pool,
instanceId: "dwh",
query: "SELECT codice, ragione_sociale FROM clienti WHERE codice IN (@chiavi)",
keyColumn: "codice",
}),
key: (r) => r.codiceCliente,
merge: (rows, r) => ({ ...r, cliente: rows[0] ?? null }),
onNotFound: "error", // un cliente mancante è un errore di dati
outputSchema: rigaConClienteSchema,
retry: { attempts: 3 },
}, { deadLetter: scarti })Da un'API REST, una GET per chiave:
ts
.lookup<Riga, RigaConOrdine>("righe", "ordine", {
resolver: httpLookup({
mode: "per-key",
config: loadHttpRegistry().get("gestionale"),
instanceId: "gestionale",
path: (id) => `/ordini/${id}`,
response: ordineSchema,
}),
key: (r) => r.ordineId,
merge: (rows, r) => ({ ...r, ordine: rows[0] ?? null }),
})Errori
| Caso | Esito |
|---|---|
chiave non trovata, onNotFound: "null" | il record passa, merge riceve [] |
chiave non trovata, onNotFound: "error" | LookupError |
| errore del sistema esterno | LookupError; se transitorio si ritenta con retry |
LookupError con deadLetter | il record esce come DeadLetter, lo stream continua |
LookupError sotto un try-catch | ramo catch |
| altrimenti | fail-fast |
Un errore del round-trip riguarda tutto il batch: con batchSize: 1 l'errore resta sul singolo record.
Nel DAG e nel report
Il nodo mostra il descriptor del resolver (istanza, query o endpoint; mai credenziali). Senza outputSchema eredita le colonne dell'input, anche se merge ha cambiato la forma del record. Nel report, retries conta i tentativi ripetuti.
In nodeReports il nodo porta i propri contatori sotto lookup, senza cablare nulla:
| Campo | Significato |
|---|---|
queries | round-trip effettivi verso il sistema esterno |
keysResolved | chiavi distinte per cui il sistema ha restituito almeno una riga |
cacheHits | chiavi servite dalla cache invece che da una query |
notFound | record per cui nessuna chiave ha prodotto righe |
retries | round-trip ripetuti dopo un errore transitorio |
Sono i contatori della run: lo stesso grafo rieseguito riparte da zero. Un writeReport passato al nodo li sostituisce, non li unisce.
La cache vive quanto una chiamata della transform. Quando il runner isola il nodo per record (una deadLetter propria, recordIds, un try-catch in modalità per-record) ogni record ha la sua: cacheHits resta a zero e una chiave ripetuta conta di nuovo in queries e keysResolved.
Righe per record: recordReport
I contatori dicono quanti record sono passati, non quali né con che esito. Per quello c'è recordReport, un'opzione del nodo che vale uguale su lookup, ai e action:
ts
.lookup("ordini", "clienti", config, {
recordReport: {
key: "ordini", // default "records"
row: (output, input) => ({ codice: input.codice, cliente: output.cliente }),
},
})rowriceve l'uscita del nodo e il record che l'ha prodotta, e restituisce un oggetto a campi semplici (stringa, numero, booleano,null). L'app rende l'elenco come tabella.- Le righe stanno sotto
key, accanto ai contatori.name,success,lookup,rowsOmittederowsFailedsono chiavi riservate. - Con un
writeReportsullo stesso nodo il grafo non si compone: quello del nodo sostituirebbe tutto il report, e le righe non comparirebbero. - Un record che fallisce non ha riga: lo riportano il ramo
catche la dead-letter. - Al più
maxRowsrighe per run (default 500, un intero ≥ 1), le altre si contano inrowsOmitted; ogni stringa si taglia a 500 caratteri. Unarowche lancia salta la riga, con un warning, e la conta inrowsFailed. - Il report è persistito:
rowsceglie i campi che servono a capire l'esito, mai il record intero.
Dal vivo
Nei contatori dal vivo il lookup ha un in lavorazione: i record della finestra che ha in mano. Salgono mentre la finestra si riempie, restano fermi finché il round-trip non risponde, e scendono a zero man mano che i record escono. Sono record, non chiamate: un round-trip copre una finestra intera, e un resolver che dentro fa più richieste in parallelo (httpLookup per chiave, con concurrency) non cambia il numero. Quando il runner isola il nodo per record la finestra è da uno, e il valore non supera uno. Un numero fermo a batchSize vuol dire che il sistema esterno sta rispondendo lento (convenzione del report).
Anti-pattern
- Una query o un
fetchdentro unatransform: è esattamente ciò che illookuprende visibile. - Un retry fatto a mano nel resolver: ritenta anche gli errori permanenti e non compare nel report.
- Scritture nel resolver: la query è validata come sola lettura, e una scrittura è un'
action.

