Client interfaces (DI)
Perché esistono
ImportExportApi è una classe concreta che parla con i microservizi node-excel-import / node-excel-export. Per usare la libreria con un backend diverso (SDK custom, GraphQL, mock di test, contratto wire alternativo) senza ereditare dalla classe, basta implementare le 3 interfacce esposte.
I componenti ImportWizard e ExportDialog accettano la prop api: ImportExportClient (l'unione delle 3 interfacce) — accetta quindi qualsiasi oggetto che soddisfi i contratti.
I 3 contratti
IImportClient
interface IImportClient {
uploadFile(
file: File,
risorsa: string,
opzioniCsv?: Partial<CsvOpzioni>,
valoriFissi?: Record<string, unknown> // dalla 0.2.5
): Promise<UploadResult>
testImport(sessionId: string, mappings: MappingColonna[]): Promise<TestImportResult>
executeImport(sessionId: string, mappings: MappingColonna[]): Promise<ImportResult>
getImportStatus(importId: string): Promise<ImportProgresso>
uploadImages(sessionId: string, files: File[]): Promise<{ caricati: number; errori: string[] }>
downloadTemplate(risorsa: string, formato: string): Promise<Blob>
getImportHistory(risorsa?: string): Promise<ImportHistoryItem[]>
getImportFieldMetadata(risorsa: string): Promise<CampoMetadato[]>
cancelUpload(): void
}
Operazioni di import: upload, dry-run, esecuzione, polling status, immagini collegate, template d'esempio, storico, metadata campi destinazione.
valoriFissi arriva da ImportWizard.valoriFissi (o da useImport().valoriFissi): vanno applicati a ogni riga importata. ImportExportApi li manda nel campo multipart fixedValues.
IExportClient
interface IExportClient {
executeExport(request: ExportRequest): Promise<ServerAsyncExportPayload | Blob>
getExportStatus(exportId: string): Promise<ExportProgresso>
downloadExport(url: string): Promise<Blob>
getExportFieldMetadata(risorsa: string): Promise<CampoMetadato[]>
cancelExport(): void
}
Operazioni di export: esecuzione (sincrona/asincrona), polling, download del file generato, metadata campi esportabili.
request.ordinamento contiene tutti i criteri, in ordine di priorità, ognuno con nulls facoltativo ('first' | 'last', dalla 0.7.0); request.filtri va passato al backend così com'è. Formati: ExportDialog › Filtri e ordinamento.
ITemplateClient
interface ITemplateClient {
getTemplates(risorsa?: string): Promise<ModelloExport[]>
createTemplate(
template: Omit<ModelloExport, 'id' | 'dataCreazione' | 'dataModifica' | 'creatoDa'>
): Promise<ModelloExport>
updateTemplate(id: string, template: Partial<ModelloExport>): Promise<ModelloExport>
deleteTemplate(id: string): Promise<void>
}
CRUD dei template di export salvati (modelli riusabili con campi/filtri/ordinamento).
ImportExportClient (union)
type ImportExportClient = IImportClient & IExportClient & ITemplateClient
Tipo accettato da ImportWizard.api / ExportDialog.api. Le prop sui componenti smart richiedono l'unione completa: anche se un caso d'uso esercita solo le funzioni di import, l'oggetto deve esporre comunque tutti i 18 metodi (9 di import, 5 di export, 4 di template) per soddisfare il tipo. I composable chiedono solo la parte che usano: useImport(api?: IImportClient), useExport(api?: IExportClient & ITemplateClient), useExportTemplates(api: ITemplateClient).
ApiError (esportato dalla 0.8.0, new ApiError(status, messaggioServizio, message)) ottiene in ExportDialog gli stessi messaggi d'errore di ImportExportApi; un altro Error mostra il proprio message dopo l'apertura («Impossibile salvare il modello. …»). Vedi Gestione errori.as ImportExportClient un oggetto che implementa solo i metodi necessari. Vedi sezione Mock per test.Esempio: mock per test
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { ImportWizard, type ImportExportClient } from '@pzeta/vue-importexport'
const mockClient = {
// Solo i metodi che il test esercita
uploadFile: vi.fn().mockResolvedValue({
sessionId: 'sess-1',
righe: 100,
colonne: ['nome', 'cognome', 'email'],
}),
getImportFieldMetadata: vi.fn().mockResolvedValue([
{ nome: 'nome', etichetta: 'Nome', tipo: 'string', obbligatorio: true },
{ nome: 'cognome', etichetta: 'Cognome', tipo: 'string', obbligatorio: true },
{ nome: 'email', etichetta: 'Email', tipo: 'email', obbligatorio: false },
]),
testImport: vi.fn().mockResolvedValue({ righeValide: 98, righeErrore: 2, errori: [] }),
executeImport: vi.fn().mockResolvedValue({
stato: 'completato',
righeImportate: 98,
righeScartate: 2,
}),
cancelUpload: vi.fn(),
// I restanti metodi delle 3 interfacce non sono toccati nel test
} as unknown as ImportExportClient
it("esegue l'upload e il test", async () => {
const wrapper = mount(ImportWizard, {
props: {
visible: true,
baseUrl: 'http://localhost',
risorsa: 'anagrafica',
api: mockClient,
},
})
// ... interagisci col wizard
expect(mockClient.uploadFile).toHaveBeenCalled()
})
Esempio: backend GraphQL
Skeleton per backend GraphQL — i 3 contratti vengono implementati separatamente e composti:
import type {
IImportClient,
IExportClient,
ITemplateClient,
ImportExportClient,
} from '@pzeta/vue-importexport'
import type { GraphQLClient } from 'graphql-request'
class GraphQLImportClient implements IImportClient {
constructor(private readonly gql: GraphQLClient) {}
async uploadFile(file, risorsa, opzioniCsv, valoriFissi) {
// GraphQL multipart upload spec
const { upload } = await this.gql.request(UPLOAD_MUTATION, { file, risorsa, opzioniCsv, valoriFissi })
return upload
}
async testImport(sessionId, mappings) {
const { test } = await this.gql.request(TEST_MUTATION, { sessionId, mappings })
return test
}
async executeImport(sessionId, mappings) {
const { execute } = await this.gql.request(EXECUTE_MUTATION, { sessionId, mappings })
return execute
}
async getImportStatus(importId) {
const { status } = await this.gql.request(STATUS_QUERY, { importId })
return status
}
async uploadImages(sessionId, files) { /* ... */ }
async downloadTemplate(risorsa, formato) {
// GraphQL non gestisce binari: fallback a endpoint REST/CDN dedicato
const res = await fetch(`/import/template/${risorsa}?formato=${formato}`)
return res.blob()
}
async getImportHistory(risorsa) { /* ... */ }
async getImportFieldMetadata(risorsa) { /* ... */ }
cancelUpload() { /* GraphQL request abort */ }
}
class GraphQLExportClient implements IExportClient { /* ... */ }
class GraphQLTemplateClient implements ITemplateClient { /* ... */ }
// Composizione delle 3 implementazioni
function createGraphQLClient(gql: GraphQLClient): ImportExportClient {
const importer = new GraphQLImportClient(gql)
const exporter = new GraphQLExportClient(gql)
const templates = new GraphQLTemplateClient(gql)
// I metodi di classe stanno sul prototipo: Object.assign / spread non li copierebbero.
// Si delega esplicitamente, così `this` resta quello di ogni implementazione.
return {
uploadFile: (...a) => importer.uploadFile(...a),
testImport: (...a) => importer.testImport(...a),
executeImport: (...a) => importer.executeImport(...a),
getImportStatus: (...a) => importer.getImportStatus(...a),
uploadImages: (...a) => importer.uploadImages(...a),
downloadTemplate: (...a) => importer.downloadTemplate(...a),
getImportHistory: (...a) => importer.getImportHistory(...a),
getImportFieldMetadata: (...a) => importer.getImportFieldMetadata(...a),
cancelUpload: () => importer.cancelUpload(),
executeExport: (...a) => exporter.executeExport(...a),
getExportStatus: (...a) => exporter.getExportStatus(...a),
downloadExport: (...a) => exporter.downloadExport(...a),
getExportFieldMetadata: (...a) => exporter.getExportFieldMetadata(...a),
cancelExport: () => exporter.cancelExport(),
getTemplates: (...a) => templates.getTemplates(...a),
createTemplate: (...a) => templates.createTemplate(...a),
updateTemplate: (...a) => templates.updateTemplate(...a),
deleteTemplate: (...a) => templates.deleteTemplate(...a),
}
}
const myClient = createGraphQLClient(myGraphQLClient)
<ImportWizard :api="myClient" base-url="" risorsa="anagrafica" />
downloadTemplate, downloadExport e executeExport (per Blob immediati) a endpoint REST/CDN dedicati.Esempio: estensione di ImportExportApi
Per estendere il client default (es. aggiungere caching, retry, header custom) puoi sottoclassarlo:
import { ImportExportApi, type ImportExportApiOptions } from '@pzeta/vue-importexport'
import type { CampoMetadato } from '@pzeta/vue-importexport'
class CachedImportExportApi extends ImportExportApi {
private metadataCache = new Map<string, CampoMetadato[]>()
constructor(baseUrl: string, options?: ImportExportApiOptions) {
super(baseUrl, options)
}
async getImportFieldMetadata(risorsa: string): Promise<CampoMetadato[]> {
if (this.metadataCache.has(risorsa)) {
return this.metadataCache.get(risorsa)!
}
const fields = await super.getImportFieldMetadata(risorsa)
this.metadataCache.set(risorsa, fields)
return fields
}
}
const api = new CachedImportExportApi(baseUrl)
<ImportWizard :api="api" :base-url="baseUrl" risorsa="anagrafica" />
Wizard custom: il client va ai composable
IMPORT_EXPORT_API_KEY non è esportato dal pacchetto: il client lo forniscono soltanto ImportWizard ed ExportDialog, dalla prop api o da baseUrl. Per comporre un wizard fuori da quei due componenti non serve: gli step (ImportStepUpload, ImportStepMapping, …) sono presentazionali, ricevono stato con le props e notificano con gli eventi, e i composable accettano il client come argomento.
<script setup lang="ts">
import { ImportExportApi, useImport, ImportStepUpload } from '@pzeta/vue-importexport'
const api = new ImportExportApi(import.meta.env.VITE_IMPORTEXPORT_API_URL)
// Niente provide: il client arriva come argomento
const { file, formato, risorsa, opzioniCsv, loading, errore, setFile, upload, cancelUpload, downloadTemplate } =
useImport(api)
risorsa.value = 'anagrafica'
// Come ImportWizard: scelto il file, l'upload parte subito
async function onSelectFile(f: File) {
setFile(f)
await upload()
}
</script>
<template>
<ImportStepUpload
:file="file"
:formato="formato"
:risorsa="risorsa"
:opzioni-csv="opzioniCsv"
:loading="loading"
:errore="errore"
@select-file="onSelectFile"
@update:opzioni-csv="opzioniCsv = $event"
@cancel-upload="cancelUpload"
@download-template="downloadTemplate"
/>
</template>
Props ed eventi di ogni step: Step dell'import.
ImportHistory da solo non funziona. È l'unico componente che recupera il client con useApi() e non ha una prop api; nessun contenitore della libreria lo monta e la chiave non è esportata, quindi montato in una pagina lancia «ImportExportApi non disponibile». Per mostrare lo storico usa useImportHistory(risorsa, client) con una tabella dell'app.TypeScript
Tutti i tipi rilevanti sono esportati dal package root:
import type {
IImportClient,
IExportClient,
ITemplateClient,
ImportExportClient,
ImportExportApiOptions,
} from '@pzeta/vue-importexport'
import { ImportExportApi } from '@pzeta/vue-importexport'