Skip to content

0042. Sorgente a cartella e sink multi-file su Noeva

  • Stato: Accettata
  • Data: 2026-09-09
  • Decisori: Team BI/ETL
  • Ticket: IGGN-10

Contesto

I connettori Noeva esistenti assumono un file solo, noto in anticipo:

  • noevaFileSource risolve un file per fileId, nome esatto o ricerca fuzzy.
  • noevaFileSink scrive un file con nome fisso, da uno stream di byte.

Va bene per una pipeline che legge un file e ne scrive un altro. Non basta per la forma di lavoro che ricorre appena il flusso diventa periodico: «prendi i file che una cartella ha ricevuto e che non hai ancora elaborato, e riscrivili uno per uno».

Tre cose mancano, e sono indipendenti fra loro:

  1. Il perimetro è una cartella, non un nome. I nomi non sono prevedibili.
  2. Serve sapere cosa è già stato fatto, altrimenti ogni run rifà tutto.
  3. L'uscita è N file, ciascuno col proprio nome e la propria metadata.

Decisione

1. noevaFolderSource — il perimetro è la cartella, il filtro è un marcatore

ts
noevaFolderSource(client, {
  folderId,
  includeSubfolders,
  skipIfMetadataKey, // salta i file che portano già questa chiave
  maxFiles, // tetto per run
});

«Non ancora elaborato» si esprime con un marcatore nella metadata del file, non con current_version === 1. Quest'ultimo sembra gratis e si rompe in tre modi prevedibili:

  • una correzione caricata a mano come v2 non verrebbe mai elaborata;
  • un'elaborazione fallita dopo l'upload risulterebbe fatta;
  • «elaborato bene» ed «elaborato in errore» diventerebbero indistinguibili.

Il marcatore invece porta con sé l'esito, e toglierlo è il modo documentato di chiedere una rielaborazione.

Costo accettato consapevolmente: una GET /api/files/:fileId per file. L'endpoint della cartella è servito da una RPC che restituisce solo id, name, folder_id, created_at, added_at, folder_name — niente current_version e niente metadata. A pochi file al giorno è irrilevante, ma è una scelta, non una svista, e sta scritta nel codice.

maxFiles ha un default: la prima run su una cartella con anni di storico non deve poter partire per sbaglio su tutto.

2. noevaFilesSink — un file per record, con la sua metadata

ts
noevaFilesSink(client, { mode: "new-version" | "overwrite", folderId? })
  : Sink<{ fileName, bytes, mimeType?, metadata? }>

Due cose in più rispetto al sink esistente, entrambe necessarie:

loadOne, quindi il sink è isolabile per-record (ADR-0032). Dentro uno scope guard il fallimento dell'upload di un file finisce sul ramo catch portando con sé il file, e gli altri passano. Su un sink che carica N file in una run è la differenza fra «un file è andato storto» e «la giornata è persa».

La metadata viene fusa, non sostituita. upload/prepare rimpiazza la bag metadata (file-upload.controller.ts: metadata: { ...metadataBase, … }, dove metadataBase è solo ciò che il chiamante manda). Senza rilettura e merge, ogni nuova versione azzererebbe le chiavi già presenti — incluso il marcatore scritto dalla run precedente, cioè esattamente il meccanismo del punto 1. La rilettura non costa una chiamata in più nel caso che conta: il file esiste già, e lo si stava comunque interrogando.

L'hash-compare di noevaFileSink (shouldSkipIdenticalVersion) è riusato tale e quale: stesso contenuto ⇒ nessuna versione nuova. Un file saltato non viene contato fra gli scritti — contarlo renderebbe il report indistinguibile da una run che ha davvero caricato.

2-bis. folderId che funziona davvero (aggiunto il 2026-09-10)

upload/prepare non assegna un file a una cartella: salva metadata.folderId e nient'altro — nessun controller e nessun trigger lo leggono. Un file nuovo finisce quindi nella radice del workspace, qualunque folderId gli si passi.

noevaFilesSink ora, dopo l'upload, chiama POST /api/folders/:folderId/files/:fileId, che è l'unico modo di impostare l'appartenenza. La chiamata è idempotente lato server (un file già presente è «already exists», non un errore), quindi si fa sempre quando folderId è impostato, senza distinguere file nuovi da versioni.

folderId non viene più scritto nella metadata: faceva sembrare impostata una cartella che non lo era.

noevaFileSink (il sink a file singolo) aveva lo stesso difetto ed è stato corretto allo stesso modo: dopo completeUpload chiama la stessa rotta, e folderId è uscito dalla metadata. I due sink ora si comportano uguale, anche nei due casi di bordo:

  • un file saltato dall'hash-compare non viene assegnato: non è stato caricato, quindi resta dove si trova;
  • un'assegnazione fallita è un errore del sink (LoadError), anche se i byte sono già stati caricati: riportare il file come scritto nasconderebbe proprio il difetto che questa chiamata chiude.

3. Nessuna deprecazione

noevaFileSource e noevaFileSink restano. Sono più semplici e restano la scelta giusta quando il file è uno e il nome è noto: sostituirli con la versione generale peggiorerebbe i chiamanti esistenti senza dare niente in cambio.

Alternative considerate

  • Estendere noevaFileSource con un folderId opzionale. Scartata: le due modalità hanno opzioni disgiunte (fileId/fuzzyQuery contro skipIfMetadataKey/maxFiles) e un contratto che ne ammette una sola alla volta è un contratto che si documenta con una nota invece che con i tipi.
  • current_version === 1 come marcatore. Scartata per i tre modi di rompersi elencati sopra.
  • Marcatore in una tabella nostra invece che nella metadata del file. Scartata: introdurrebbe uno stato da tenere allineato a un sistema che non controlliamo, e la prima riconciliazione mancata fra i due sarebbe silenziosa. La metadata sta sul file, quindi non può divergere dal file.
  • Un PATCH della metadata senza caricare byte. Non esiste: sulle API dei file c'è solo PATCH /api/files/:fileId/rename. Il marcatore si può scrivere solo passando da upload/prepare, ed è il motivo per cui una run che salta l'upload salta anche il marcatore.

Conseguenze

Positive

  • Una pipeline periodica su cartella diventa esprimibile senza codice ad hoc.
  • Il fallimento di un file non porta giù gli altri.
  • «Cosa è già stato elaborato» è un'informazione che vive sul file, leggibile anche da fuori dalla pipeline.

Negative / costi

  • Una GET per file nel filtro d'ingresso: lineare nel numero di file della cartella, non in quelli da elaborare. Su una cartella molto grande andrà ripensato — il posto giusto sarebbe la RPC get_folder_files, che dovrebbe restituire la metadata.
  • Il marcatore si scrive solo caricando. Un file il cui output è identico all'ultima versione viene saltato dall'hash-compare e non riceve il marcatore: verrà rielaborato alla run successiva, con lo stesso esito e senza creare versioni. Non è dannoso, è lavoro ripetuto — e va scritto nel runbook, non scoperto in produzione.

Cosa diventa più difficile da cambiare

Il nome della chiave del marcatore diventa parte del contratto col cliente: una volta che i file in cartella la portano, cambiarla significa che tutto lo storico risulta non elaborato. Va trattata come un identificatore stabile, non come un dettaglio interno.

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