Services

ImportExportApi

Client HTTP per i microservizi node-excel-import e node-excel-export.

Panoramica

ImportExportApi è il client HTTP usato dalla libreria per interagire con i due microservizi backend:

  • node-excel-import — endpoint {baseUrl}/import/*
  • node-excel-export — endpoint {baseUrl}/export/*

Internamente usa fetch con AbortController per supportare l'annullamento di upload ed export. Espone metodi tipizzati per upload, test, esecuzione, polling, metadata, template e storico.

Costruttore

new ImportExportApi(baseUrl: string, options?: ImportExportApiOptions)
ParametroTipoDescrizione
baseUrlstringURL base — gli slash finali vengono normalizzati
optionsImportExportApiOptionsPath personalizzati per gli endpoint (opzionale)

ImportExportApiOptions

interface ImportExportApiOptions {
  /** Path base degli endpoint di import (default: '/import'). */
  importPath?: string
  /** Path base degli endpoint di export (default: '/export'). */
  exportPath?: string
}

Il costruttore concatena baseUrl + path normalizzando le slash:

// Default
new ImportExportApi('https://api.example.com')
// → importUrl = 'https://api.example.com/import'
// → exportUrl = 'https://api.example.com/export'

// Custom path
new ImportExportApi('https://api.example.com', {
  importPath: '/v2/import',
  exportPath: '/v2/export',
})
// → importUrl = 'https://api.example.com/v2/import'
// → exportUrl = 'https://api.example.com/v2/export'
Fino alla 0.2.5 i default erano /api/import e /api/export. Un consumer che dipende da quel prefisso lo passa esplicitamente: new ImportExportApi(baseUrl, { importPath: '/api/import', exportPath: '/api/export' }).

Implementa 3 contratti

ImportExportApi implementa simultaneamente le tre interfacce IImportClient, IExportClient e ITemplateClient. La type union ImportExportClient è il tipo accettato dalle prop api di ImportWizard / ExportDialog.

Per backend custom (mock di test, GraphQL, SDK alternativi) puoi implementare le interfacce senza ereditare dalla classe concreta. Vedi Client interfaces.

Metodi import

uploadFile(file, risorsa, opzioniCsv?, valoriFissi?)

uploadFile(
  file: File,
  risorsa: string,
  opzioniCsv?: Partial<CsvOpzioni>,
  valoriFissi?: Record<string, unknown>
): Promise<UploadResult>

Upload del file via multipart/form-data su POST /import/upload, con i campi file, risorsa, opzioniCsv (JSON, se presenti) e fixedValues (JSON di valoriFissi, solo se non vuoto: il servizio lo fonde su ogni riga; dalla 0.2.5). Crea un AbortController interno usato da cancelUpload(). Se annullato, lancia Error("Upload annullato dall'utente").

testImport(sessionId, mappings)

testImport(sessionId: string, mappings: MappingColonna[]): Promise<TestImportResult>

Dry-run su POST /import/test. Body: { sessionId, mappings }.

executeImport(sessionId, mappings)

executeImport(sessionId: string, mappings: MappingColonna[]): Promise<ImportResult>

Esecuzione effettiva su POST /import/execute. Body: { sessionId, mappings }.

getImportStatus(importId)

getImportStatus(importId: string): Promise<ImportProgresso>

Polling status di un import asincrono — GET /import/status/{importId}.

uploadImages(sessionId, files)

uploadImages(
  sessionId: string,
  files: File[]
): Promise<{ caricati: number; errori: string[] }>

Upload immagini collegate alla sessione di import — POST /import/images (multipart con images[]).

downloadTemplate(risorsa, formato)

downloadTemplate(risorsa: string, formato: string): Promise<Blob>

Scarica template d'esempio della risorsa — GET /import/template/{risorsa}?formato={formato}. Restituisce un Blob.

getImportHistory(risorsa?)

getImportHistory(risorsa?: string): Promise<ImportHistoryItem[]>

Storico importazioni — GET /import/history?risorsa=.... Senza risorsa restituisce tutto lo storico accessibile all'utente.

cancelUpload()

cancelUpload(): void

Annulla l'upload in corso (no-op se non c'è upload attivo).

Metodi export

executeExport(request)

executeExport(
  request: ExportRequest
): Promise<ServerAsyncExportPayload | Blob>

Esecuzione export — POST /export/execute. Comportamento polimorfico:

  • Risposta 2xx con content-type binario (application/... o text/csv) → Blob
  • Risposta 202 Accepted → ServerAsyncExportPayload (export asincrono, partirà il polling)
  • Altre risposte JSON → ServerAsyncExportPayload

Mappa ExportRequest (formato libreria) sul DTO del microservizio:

ExportRequestCorpo della richiesta
risorsa, formato, campistessi nomi
nomeFilefileName (default export_<risorsa>)
modelloIdtemplateId, numerico
filtrifiltri, intatti
ordinamentoopzioni.ordinamento: tutti i criteri, con direzione in maiuscolo e nulls in maiuscolo solo se presente; senza criteri opzioni manca
// ordinamento: [{ campo: 'datagiro', direzione: 'desc' }, { campo: 'orapartenza', direzione: 'desc', nulls: 'last' }]
"opzioni": {
  "ordinamento": [
    { "campo": "datagiro", "direzione": "DESC" },
    { "campo": "orapartenza", "direzione": "DESC", "nulls": "LAST" }
  ]
}

Fino alla 0.6.0 andava solo il primo criterio; nulls va dalla 0.7.0. Ordinamento su più colonne, between e array di condizioni nei filtri richiedono node-excel-export ≥ 1.1.14, nulls la 1.1.15 (la 1.1.14 lo scarta senza errore).

Annullabile via cancelExport().

getExportStatus(exportId)

getExportStatus(exportId: string): Promise<ExportProgresso>

Polling export asincrono — GET /export/status/{exportId}.

downloadExport(url)

downloadExport(url: string): Promise<Blob>

Download del file generato. Validazione CSRF: l'origin dell'URL deve coincidere con l'origin di exportUrl, altrimenti viene rifiutato con Error("URL download non autorizzato: ...").

cancelExport()

cancelExport(): void

Annulla l'export in corso.

Metodi template

getTemplates(risorsa)

getTemplates(risorsa?: string): Promise<ModelloExport[]>

GET /export/templates?risorsa=.... Senza risorsa restituisce array vuoto. Il backend risponde con { risorsa, count, templates } o un array diretto — l'API normalizza entrambi e applica mapMsTemplateToModello per convertire dal formato microservizio (MsTemplate) al formato libreria (ModelloExport).

createTemplate(template)

createTemplate(
  template: Omit<ModelloExport, 'id' | 'dataCreazione' | 'dataModifica' | 'creatoDa'>
): Promise<ModelloExport>

POST /export/templates. La libreria mappa campi: string[] → campijson: Array<{ nomecampo, etichetta, ordinamento }>: etichetta è template.etichette[nome], l'etichetta che l'utente vede, o il nome se manca. Fino alla 0.7.0 era sempre il nome tecnico ({ "nomecampo": "datagiro", "etichetta": "datagiro" }).

updateTemplate(id, updates)

updateTemplate(
  id: string,
  updates: Partial<ModelloExport>
): Promise<ModelloExport>

PUT /export/templates/{id}. Solo i campi presenti in updates vengono inviati al server (patch semantics). Con campi, campijson porta le etichette di updates.etichette come in createTemplate.

deleteTemplate(id)

deleteTemplate(id: string): Promise<void>

DELETE /export/templates/{id}.

Metodi metadata

getImportFieldMetadata(risorsa)

getImportFieldMetadata(risorsa: string): Promise<CampoMetadato[]>

GET /import/metadata/{risorsa}/fields. Restituisce i campi destinazione disponibili per l'import.

getExportFieldMetadata(risorsa)

getExportFieldMetadata(risorsa: string): Promise<CampoMetadato[]>

GET /export/metadata/{risorsa}/fields. Restituisce i campi esportabili.

Esempio standalone

import { ImportExportApi } from '@pzeta/vue-importexport'

const api = new ImportExportApi('https://api.example.com')

// Upload + test + execute
const upload = await api.uploadFile(file, 'anagrafica', { separatore: ';' })
const test = await api.testImport(upload.sessionId, mappings)

if (test.righeErrore === 0) {
  const result = await api.executeImport(upload.sessionId, mappings)
  console.log(`Importate: ${result.righeImportate}`)
}

Esempio export con polling

// downloadBlob della libreria è interno: qui un helper dell'app
function downloadBlob(blob: Blob, nomeFile: string) {
  const url = URL.createObjectURL(blob)
  const a = Object.assign(document.createElement('a'), { href: url, download: nomeFile })
  a.click()
  URL.revokeObjectURL(url)
}

const result = await api.executeExport({
  risorsa: 'anagrafica',
  formato: 'xlsx',
  campi: ['nome', 'cognome', 'email'],
  nomeFile: 'Anagrafiche 2026-10-15',
  filtri: { attivo: true, datanascita: { operator: 'between', value: ['1980-01-01', '1989-12-31'] } },
  ordinamento: [
    { campo: 'cognome', direzione: 'asc' },
    { campo: 'email', direzione: 'asc', nulls: 'last' },
  ],
})

if (result instanceof Blob) {
  // Sincrono: salva direttamente
  downloadBlob(result, 'anagrafica.xlsx')
} else {
  // Asincrono: poll fino a completamento
  let status = result
  while (status.stato === 'in_corso') {
    await new Promise((r) => setTimeout(r, 2000))
    status = await api.getExportStatus(result.exportId)
  }
  if (status.urlDownload) {
    const blob = await api.downloadExport(status.urlDownload)
    downloadBlob(blob, status.nomeFile ?? 'export.xlsx')
  }
}

Gestione errori

Dalla 0.8.0 le risposte non-2xx lanciano ApiError, esportato dal pacchetto:

class ApiError extends Error {
  readonly name: 'ApiError'
  /** Stato HTTP; 0 se la richiesta non è arrivata al servizio */
  readonly status: number
  /** `message` del corpo della risposta, se è una stringa non vuota */
  readonly messaggioServizio: string | null
}
CampoContenuto
statusStato HTTP della risposta. 0 quando fetch fallisce per rete (servizio non raggiungibile, CORS, DNS): l'errore originale è in cause
messaggioServizioIl message al primo livello del corpo: gli errori applicativi di node-excel-export lo scrivono per l'utente (es. il 403 sulla condivisione). Il 400 di validazione ha il dettaglio annidato in error e lascia null
messageIl testo tecnico di sempre, per i log: Errore API: {status}: {message}, Errore export: …, Errore download: …, Errore eliminazione template: …. Il {message} è body.message, poi body.error, poi res.statusText
import { ApiError } from '@pzeta/vue-importexport'

try {
  await api.createTemplate(modello)
} catch (e) {
  if (e instanceof ApiError && e.status === 403) {
    console.warn(e.messaggioServizio)   // «Solo un amministratore può condividere…»
  }
}

Lo stato 0 lo danno tutte le chiamate tranne executeExport, dove un errore di rete resta l'errore di fetch. L'annullamento non diventa mai un ApiError: cancelUpload() e cancelExport() fanno lanciare Error("Upload annullato dall'utente") / Error("Export annullato dall'utente").