node-excel-import
node-excel-import
In sintesi
Microservizio di importazione massiva di dati tabellari (Excel xlsx/xls e CSV) verso il database del tenant ditta. Espone una pipeline asincrona — upload → parsing → validazione → trasformazione → arricchimento → persistenza — con tracking dello stato del job (pending → processing → completed/failed) e supporto a lookup table riusabili per mapping codici. Nel grafo dei servizi è la porta d'ingresso "ETL leggero" per popolare risorse PostgreSQL a partire da fogli di calcolo prodotti dagli operatori o da sistemi esterni. Espone eventi NATS al completamento dei job e si registra come modulo orchestrator per workflow inter-servizio. Permette anche il download delle righe rifiutate come file Excel/CSV per la correzione manuale.
Funzionalità principali
- Upload e parsing di file
xlsx,xlsecsvviamultipart/form-data, con selezione del foglio, salto righe header, limite righe e validazione schema tramite Zod - Preview senza commit: ispezione dei primi N record (max 100) e dei nomi dei fogli, utile al frontend per costruire il mapping colonne prima dell'import effettivo
- Column mapping dichiarativo da colonna sorgente a campo target, con flag
requiredper i campi obbligatori - Trasformazioni dati componibili in catena (
uppercase,lowercase,trim,parse_number,parse_date,format_date,format_currency,round,regex_replace,default_value,required,conditional,lookup,formula) applicate per ordine configurabile - Lookup table persistite su PostgreSQL con CRUD completo, ricerca exact match e fuzzy match (distanza di Levenshtein con estensione
pg_trgm) - Risoluzione External ID: una colonna del foglio può fungere da chiave naturale; il servizio costruisce in batch il mapping External ID → primary key per decidere automaticamente fra
INSERTeUPDATE(upsert) - Data enrichment opzionale verso provider HTTP esterni configurabili, con throttling a batch per non saturare i servizi remoti
- Job tracking completo: contatori
processedRows/totalRows/rowsCreated/rowsUpdated/rowsFailed, errori e timestamp, interrogabili per id, per stato o come elenco dei più recenti - Auto-migration PostgreSQL idempotente al bootstrap, con
pg_advisory_lockper gestire più repliche sullo stesso schema - Download righe rifiutate via
GET /import/jobs/{jobId}/rejected-rows?format=xlsx|csv&disposition=attachment|inline: produce un file con le sole righe scartate, preservando le colonne originali e aggiungendo una colonna_errorscon il messaggio di errore (pensato per il flusso "correggi e re-importa") - Pubblicazione eventi NATS su JetStream al termine del job — subject
services.excel-import.job.completed.{tenant}.{jobId}eservices.excel-import.job.failed.{tenant}.{jobId}con payload allineato alla convenzione PZeta delle altre flotte event-driven - Registrazione come modulo orchestrator (
@pzeta/orchestrator-node) con due task NATS request/reply:start-import-job(parzialmente integrato: lo scaricamento del file danode-storageviaidallegatonon è ancora completato e il task restituisce un errore esplicito) eget-import-status(operativo, espone lo stato corrente del job)
Architettura
Stack: Fastify v5 · Inversify (DI con constructor injection e decoratori @injectable()/@inject()) · @fastify/multipart per gli upload · exceljs per parsing Excel · zod per la validazione di request e righe importate · date-fns per le trasformazioni temporali · pg (driver nativo) verso PostgreSQL · @pzeta/fastify-utils (loggingPlugin, errorHandlerPlugin, healthcheckPlugin, metricsPlugin, openapiPlugin, securityPlugin con rate limit) · @pzeta/log per logging strutturato e correlation ID · @pzeta/orchestrator-node per la registrazione del modulo presso node-orchestrator · nats.js (driver ufficiale) per JetStream pub/sub.
Layout DDD (src/):
| Layer | Contenuto |
|---|---|
Domain/ | Entità (ImportJob, LookupTable, ValidationResult), value object (ImportJobId, FileName, ColumnMapping, TransformationRule), interfacce repository |
Application/ | ImportService (facade), ImportOrchestrator (pipeline + emissione eventi NATS a fine job), ImportJobService (lifecycle stati), FileSystemService, LookupTableService, RejectedRowsService (esportazione righe rifiutate in xlsx/csv), DTO (ImportOptions, RejectedRowsExportOptions) e port (IExcelParser, IDataTransformer, IDataEnrichmentService, IJobEventPublisher), eccezione NoRejectedRowsException |
Infrastructure/ | Adapter ExcelParser (exceljs + Zod), DataTransformer (catena di trasformazioni), DataEnrichmentService (HTTP), repository PostgreSQL (PostgresImportJobRepository, PostgresLookupTableRepository, PostgresExternalIdRepository, GenericResourceRepository, PostgresMetadataRepository), PostgresConnection, AutoMigration, DIContainer, messaging: OrchestratorModule (registra il modulo presso node-orchestrator ed espone i task NATS), JetStreamJobEventPublisher (publisher JetStream production-ready), NoopJobEventPublisher (default quando l'integrazione è disattivata), InMemoryJobEventPublisher (uso test) |
Presentation/ | ImportController (upload, preview, jobs, stats, rejected-rows) e LookupController (CRUD tabelle e record), RouteRegistration |
Pattern adottati: Facade (ImportService su ImportOrchestrator + ImportJobService), Repository con port-and-adapter sul layer dominio, Chain of Responsibility per le trasformazioni, elaborazione asincrona del job in background con aggiornamento incrementale dello stato, schema importexport autocreato e versionato tramite file 001–005 in migrations/, Publisher sul port IJobEventPublisher con implementazione swappabile (Noop → JetStream in produzione, InMemory nei test) e modulo orchestrator NATS-first che riusa l'ImportService come backend dei task handlers.
Autenticazione: il servizio si affida a nginx (auth_request → node-user-auth) e non implementa identity in proprio. Il rate limit pubblico (createRateLimitConfig.forPublicApi, finestra di 15 minuti) escludelusivamente /health e /ready. CORS è guidato da CORS_ORIGINS (whitelist).
Casi d'uso
- Caricamento anagrafiche batch: l'operatore carica un
.xlsxdi clienti/fornitori; il frontend chiamaPOST /import/previewper mostrare le prime righe e configurare il mapping colonne, poiPOST /import/uploadconcolumnMappingseenableTransformation: trueper normalizzare maiuscole, codice fiscale e date - Aggiornamento massivo via External ID: il foglio contiene una colonna
codiceesternoche mappa a record già esistenti — l'orchestrator costruisce in batch la corrispondenza External ID → ID interno e produce un mixINSERT/UPDATEinvece di duplicare le righe - Normalizzazione tramite lookup: una colonna "tipo pagamento" testuale viene risolta contro una lookup table (
POST /lookup/tables/{name}/lookup/{key}) per ottenere il codice interno; eventuali valori non noti vengono individuati via fuzzy match e segnalati nei warning del job - Arricchimento da provider esterno: per ogni riga il
DataEnrichmentServicechiama un endpoint HTTP configurato (es. validazione partita IVA, geocoding indirizzi) a batch, con pausa configurabile tra i batch per rispettare i rate limit del provider - Monitoraggio import lunghi: il frontend polla
GET /import/jobs/{jobId}per mostrare la progress bar (processedRows/totalRows) e gli aggregati finali (rowsCreated/rowsUpdated/rowsFailed) a fine job - Workflow orchestrator event-driven:
node-schedulerinvoca un workflow che chiama il taskstart-import-job, attende l'eventoservices.excel-import.job.completed.{tenant}.>su JetStream, e propaga il risultato (numero righe importate, durata, eventualeidallegatocon le righe rifiutate) anode-notificationper produrre una notifica all'operatore - Correzione manuale righe rifiutate: a fine job l'operatore scarica
/import/jobs/{jobId}/rejected-rows?format=xlsxottenendo un foglio con le sole righe scartate e una colonna_errorscon il dettaglio del motivo; le corregge in Excel, salva e re-importa il file ripulito tramitePOST /import/uploadsenza dover ricaricare l'intero dataset
Identità & esposizione
| Campo | Valore |
|---|---|
| Categoria | data-io |
| Versione cluster | 1.0.7 |
| Image | gitea.pzetatouch.it/pzeta_touch/node-excel-import:1.0.6 |
| URL pubblico | https://ditta.pzeta.it/excel-import |
| Path regex ingress | `/excel-import(/ |
| Rewrite a backend | /$2 |
| DNS interno | node-excel-import-ditta.ditta.svc.cluster.local:3000 |
| Auth nginx | auth_request → node-user-auth |
| Repository | node-excel-import |
| Endpoint REST | 14 (vedi sezione "API reference") |
Endpoint operazionali
Endpoint convenzionali esposti da tutti i microservizi PZeta basati su @pzeta/fastify-utils:
| Path pubblico | Scopo |
|---|---|
https://ditta.pzeta.it/excel-import/health | liveness probe |
https://ditta.pzeta.it/excel-import/ready | readiness probe |
https://ditta.pzeta.it/excel-import/metrics | metriche Prometheus |
https://ditta.pzeta.it/excel-import/api-docs.json | spec OpenAPI runtime (richiede OPENAPI_EXPOSE_IN_PRODUCTION=true) |
https://ditta.pzeta.it/excel-import/api-docs | Swagger UI (solo in NODE_ENV !== production) |
Configurazione
Variabili d'ambiente rilevanti per chi integra il servizio (per la lista completa vedi .env.default del repo):
| Variabile | Ruolo |
|---|---|
HOST / PORT | Bind del server (0.0.0.0:3000 in container, 127.0.0.1:3000 di default) |
NODE_ENV | production, development o test; influisce su esposizione Swagger UI e formattazione log |
DATABASE_HOST / _PORT / _NAME / _USER / _PASSWORD | Connessione PostgreSQL — job, lookup table, risorse importate, metadati, External ID |
DATABASE_POOL_MAX | Dimensione massima del pool pg (default 20) |
AUTO_MIGRATION_ENABLED | Se false salta l'auto-migration al bootstrap; di default abilitato |
ENABLE_CORS / CORS_ORIGINS | Attiva CORS e definisce la whitelist degli origin consentiti (separati da virgola) |
REVERSE_PROXY | Se valorizzato, allunga keepAliveTimeout/headersTimeout per la convivenza con nginx |
OPENAPI_EXPOSE_IN_PRODUCTION | Espone /api-docs.json anche in produzione; Swagger UI resta off in produzione |
ENRICHMENT_BATCH_PAUSE_MS | Pausa fra batch di enrichment HTTP (default 100ms, minimo 0) |
APP_URL | URL pubblico dell'applicazione, usato in alcuni link applicativi |
ORCHESTRATOR_ENABLED | Master switch dell'integrazione orchestrator + NATS. Se false (default) il servizio resta HTTP-only e usa NoopJobEventPublisher; se true apre la connessione JetStream e registra il modulo |
ORCHESTRATOR_URL | URL HTTP del node-orchestrator per la registrazione del modulo (default http://localhost:3000) |
ORCHESTRATOR_API_KEY | API key opzionale usata dal ModuleSDK per autenticarsi presso node-orchestrator |
NATS_URL | URL del server NATS su cui apre la connessione JetStream (default nats://localhost:4222) |
NATS_STREAM_NAME | Nome dello stream JetStream creato/usato per gli eventi job (default EXCEL_IMPORT_JOBS) |
NATS_STREAM_MAX_AGE_DAYS | Retention dello stream in giorni, convertita internamente in max_age nanosecondi (default 7) |
Il moduleId esposto all'orchestrator coincide con il name del package.json (node-excel-import) e non è configurabile tramite env: per pubblicare il modulo con un identificativo diverso occorre modificare il package o il blocco di bootstrap in src/index.ts.
Lo schema PostgreSQL importexport viene creato e versionato automaticamente al bootstrap dai file in migrations/ (sequenza 001–005); l'esecuzione è protetta da pg_advisory_lock per consentire l'avvio simultaneo di più repliche.
Note eventing NATS
Il servizio non è più HTTP-only: quando ORCHESTRATOR_ENABLED=true il processo apre una connessione JetStream verso NATS_URL, garantisce l'esistenza dello stream NATS_STREAM_NAME (subject filter services.excel-import.job.>, retention Limits, storage File, max_age derivato da NATS_STREAM_MAX_AGE_DAYS) e si registra come modulo presso node-orchestrator tramite ModuleSDK di @pzeta/orchestrator-node. Con ORCHESTRATOR_ENABLED=false (default attuale) il publisher viene sostituito da un NoopJobEventPublisher e il binding all'orchestrator non viene attivato, mantenendo il comportamento HTTP-only legacy.
Eventi pubblicati
Al termine di ogni job (ImportOrchestrator) il servizio pubblica un evento idempotente su JetStream:
| Subject | Quando | Payload (JSON) |
|---|---|---|
services.excel-import.job.completed.{tenant}.{jobId} | Job in stato completed | { jobId, tenant, importedRows, rejectedRows, durationMs, idallegatoRejectedRows? } |
services.excel-import.job.failed.{tenant}.{jobId} | Job in stato failed | { jobId, tenant, errorCode, errorMessage, partialRowsImported? } |
La convenzione di subject (services.excel-import.job come prefisso, segmenti completed/failed, {tenant} e {jobId} come ultimi token) è allineata a quanto consumato dal NATSInboundGateway di node-notification: di conseguenza è sufficiente sottoscrivere services.excel-import.job.completed.> o services.excel-import.job.failed.> (con filtro per tenant se necessario) per reagire al ciclo di vita degli import.
Task orchestrator esposti
Il modulo registra due task handler request/reply sul bus NATS dell'orchestrator (definiti in OrchestratorModule.ts):
| Task | Input | Output | Stato |
|---|---|---|---|
start-import-job | { fileSource: { idallegato }, options?: { sheetName, skipRows, maxRows, resourceName } } | { jobId, status: 'queued' } | Stub: il task ritorna { success: false, error: '...' } perché il download del file via idallegato da node-storage non è ancora implementato. È predisposto per l'integrazione successiva (estensione issue #16) |
get-import-status | { jobId } | { jobId, status, totalRows, processedRows, rowsCreated, rowsUpdated, rowsFailed, errorMessage? } | Operativo: mappa direttamente su ImportService.getJobStatus |
Convenzione lato scanner
Il file nats.json esposto dallo scanner dei microservizi rileva la costante services.excel-import.job definita in JetStreamJobEventPublisher.ts come subject prefix del servizio, riconoscendo i due subject *.completed.{tenant}.{jobId} e *.failed.{tenant}.{jobId} come canali outbound. Il binding di consumo (chi reagisce agli eventi) non è di competenza di questo servizio, ma del sottoscrittore (node-notification, node-scheduler, futuri consumer).
Dipendenze e dipendenti
Dipende da (servizi che questo servizio chiama):
Consumato da (chi chiama questo servizio):
frontend Vue
Infrastruttura (PostgreSQL, NATS, Redis, MinIO) non è elencata qui — vedi sezione Architettura del singolo servizio.