Skip to content

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.

0/5
dwh — external dwhDB · tabella clienti righe — source righeSQL Server cliente — lookup clientemssqlLookup fatti — sink fattiPostgreSQL scarti — dead-letter scartidead-letter
Metodo.lookup(from, id, config, { deadLetter?, fromPort?, purpose?, recordReport?, writeReport? })
Ingressi1
Uscite≥1 (fan-out libero)
Dead-lettersì, per record
Codicesrc/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 ​

CampoDefaultEffetto
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
batchSize200Chiavi per round-trip
cachetrueTiene i risultati per la durata della run
maxCacheEntries50000Oltre, 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 ​

ResolverSistemaScheda
postgresLookupPostgreSQLpostgres § Lookup
mssqlLookupSQL Serversqlserver § Lookup
mysqlLookupMySQL, MariaDBmysql § Lookup
httpLookupAPI REST (batch o per-key)http
jiraIssueLookupJira Cloud (issue per chiave)jira
jiraChangelogLookup, jiraCommentsLookup, jiraWorklogsLookupJira Cloud (liste di una issue)jira
noevaElaborazioneLookup, noevaContenutoLookupStato e testo di un file Noevanoeva

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 ​

CasoEsito
chiave non trovata, onNotFound: "null"il record passa, merge riceve []
chiave non trovata, onNotFound: "error"LookupError
errore del sistema esternoLookupError; se transitorio si ritenta con retry
LookupError con deadLetteril record esce come DeadLetter, lo stream continua
LookupError sotto un try-catchramo catch
altrimentifail-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:

CampoSignificato
queriesround-trip effettivi verso il sistema esterno
keysResolvedchiavi distinte per cui il sistema ha restituito almeno una riga
cacheHitschiavi servite dalla cache invece che da una query
notFoundrecord per cui nessuna chiave ha prodotto righe
retriesround-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 }),
  },
})
  • row riceve 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, rowsOmitted e rowsFailed sono chiavi riservate.
  • Con un writeReport sullo 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 catch e la dead-letter.
  • Al più maxRows righe per run (default 500, un intero ≥ 1), le altre si contano in rowsOmitted; ogni stringa si taglia a 500 caratteri. Una row che lancia salta la riga, con un warning, e la conta in rowsFailed.
  • Il report è persistito: row sceglie 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 fetch dentro una transform: è esattamente ciò che il lookup rende 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.

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