Services

Client interfaces (DI)

Contratti IImportClient, IExportClient, ITemplateClient per sostituire il backend senza ereditare da ImportExportApi. Pattern Dependency Injection per testing, mock e SDK custom.

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>): 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.

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.

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 / IMPORT_EXPORT_API_KEY. 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 26 metodi per soddisfare il tipo.

Stub conveniente: per implementazioni parziali (es. solo import in test), puoi castare 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) {
    // GraphQL multipart upload spec
    const { upload } = await this.gql.request(UPLOAD_MUTATION, { file, risorsa, opzioniCsv })
    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(`/api/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)

  return Object.assign({}, importer, exporter, templates) as ImportExportClient
}

const myClient = createGraphQLClient(myGraphQLClient)
<ImportWizard :api="myClient" base-url="" risorsa="anagrafica" />
Download e binari: GraphQL non è il trasporto ideale per byte stream. Anche con un client GraphQL è prassi delegare 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" />

Iniezione manuale via IMPORT_EXPORT_API_KEY

Quando usi i singoli step / composables fuori da ImportWizard/ExportDialog, fornisci il client via provide:

<script setup lang="ts">
import { provide } from 'vue'
import {
  IMPORT_EXPORT_API_KEY,
  ImportExportApi,
} from '@pzeta/vue-importexport'

const api = new ImportExportApi(import.meta.env.VITE_IMPORTEXPORT_API_URL)
provide(IMPORT_EXPORT_API_KEY, api)
</script>

<template>
  <ImportStepUpload />
  <ImportStepMapping />
  <!-- ... -->
</template>

useApi() recupera l'oggetto provisto. Il tipo della key è InjectionKey<ImportExportClient> — quindi puoi fornire qualsiasi implementazione delle 3 interfacce, non solo la classe ImportExportApi.

TypeScript

Tutti i tipi rilevanti sono esportati dal package root:

import type {
  IImportClient,
  IExportClient,
  ITemplateClient,
  ImportExportClient,
  ImportExportApiOptions,
} from '@pzeta/vue-importexport'

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