Services

AttachmentAdapter

Interfaccia Strategy del data source. Implementala per backend non-axios (GraphQL, fetch, tRPC, mock).

Panoramica

AttachmentAdapter è il contratto Strategy che separa la libreria dal trasporto. Ogni implementazione deve garantire la stessa semantica anche se il backend espone un protocollo diverso.

Per il caso REST + axios (90% dei casi) usa la factory pronta createAxiosAdapter. Implementa l'interfaccia direttamente quando hai GraphQL, fetch puro, tRPC, in-memory mock per Storybook, ecc.

Interfaccia

interface AttachmentAdapter {
  /** Lista attachments filtrati dalla query opzionale. */
  list(query?: AttachmentQuery): Promise<Attachment[]>

  /** Recupera un singolo attachment per id. */
  get(id: string): Promise<Attachment>

  /** Carica un nuovo file e ritorna l'attachment creato. */
  upload(params: AttachmentUploadParams): Promise<Attachment>

  /** Scarica il contenuto binario di un attachment. */
  download(id: string, disposition?: AttachmentDisposition): Promise<Blob>

  /** Elimina un singolo attachment. */
  delete(id: string): Promise<void>

  /** Elimina più attachments in un'unica chiamata. */
  deleteMany(ids: string[]): Promise<void>

  /** [Admin, opzionale] Lista i file orfani. */
  findOrphaned?(): Promise<OrphanedFilesResponse>

  /** [Admin, opzionale] Esegue la pulizia dei file orfani; dryRun=true per simulare. */
  cleanOrphaned?(dryRun?: boolean): Promise<CleanupResponse>
}

Tipi correlati

interface Attachment {
  id: string
  fileName: string
  fileType: string   // MIME type
  size: number       // bytes
  uploadDate: string // ISO 8601
  thumbnailUrl?: string
}

interface AttachmentQuery {
  referenceTable?: string
  referenceColumn?: string
  referenceValue?: string | number
  batchSize?: number | null
  storagePath?: string
}

interface AttachmentUploadParams {
  file: File | Blob | Buffer
  filename: string
  mimeType: string
  referenceTable?: string
  referenceColumn?: string
  referenceValue?: string | number
  storagePath?: string
}

type AttachmentDisposition = 'inline' | 'attachment'

interface OrphanedFilesResponse {
  found: number
  files: { type: 'storage_only' | 'database_only'; id: string; path?: string }[]
}

interface CleanupResponse {
  cleaned: number
  errors: { id: string; error: string }[]
}

Esempio: adapter mock in-memory

Utile per test, Storybook, o demo offline.

import type {
  Attachment,
  AttachmentAdapter,
  AttachmentQuery,
  AttachmentUploadParams
} from '@pzeta/vue-attachment'

export function createMockAdapter(seed: Attachment[] = []): AttachmentAdapter {
  let store: Attachment[] = [...seed]

  return {
    async list(_query?: AttachmentQuery) {
      return [...store]
    },
    async get(id) {
      const found = store.find(a => a.id === id)
      if (!found) throw new Error(`Attachment ${id} not found`)
      return found
    },
    async upload(params: AttachmentUploadParams) {
      const created: Attachment = {
        id: crypto.randomUUID(),
        fileName: params.filename,
        fileType: params.mimeType,
        size: (params.file as Blob).size ?? 0,
        uploadDate: new Date().toISOString()
      }
      store = [...store, created]
      return created
    },
    async download(id) {
      const att = await this.get(id)
      return new Blob([`mock content of ${att.fileName}`], { type: att.fileType })
    },
    async delete(id) {
      store = store.filter(a => a.id !== id)
    },
    async deleteMany(ids) {
      const set = new Set(ids)
      store = store.filter(a => !set.has(a.id))
    }
  }
}

Uso:

import { createApp } from 'vue'
import { VueAttachmentPlugin } from '@pzeta/vue-attachment'
import { createMockAdapter } from './mock-adapter'

createApp(App)
  .use(VueAttachmentPlugin, { adapter: createMockAdapter() })
  .mount('#app')

Esempio: adapter GraphQL

Skeleton per backend GraphQL. Adatta query/mutation alle tue:

import type { AttachmentAdapter } from '@pzeta/vue-attachment'
import type { GraphQLClient } from 'graphql-request'

export function createGraphQLAdapter(client: GraphQLClient): AttachmentAdapter {
  return {
    async list(query) {
      const data = await client.request(LIST_ATTACHMENTS, query)
      return data.attachments
    },
    async get(id) {
      const data = await client.request(GET_ATTACHMENT, { id })
      return data.attachment
    },
    async upload({ file, filename, mimeType, ...refs }) {
      // GraphQL multipart upload (graphql-multipart-request-spec)
      const data = await client.request(UPLOAD_ATTACHMENT, { file, filename, mimeType, ...refs })
      return data.uploadAttachment
    },
    async download(id) {
      // GraphQL non gestisce nativamente il binario: fallback REST per il blob
      const res = await fetch(`/api/attachments/${id}/download`)
      return await res.blob()
    },
    async delete(id) { await client.request(DELETE_ATTACHMENT, { id }) },
    async deleteMany(ids) { await client.request(DELETE_MANY, { ids }) }
  }
}
Download e binari: GraphQL non è il trasporto ideale per byte stream. Anche con un adapter GraphQL è prassi delegare il download a un endpoint REST/CDN dedicato.

Endpoint admin opzionali

findOrphaned e cleanOrphaned sono opzionali: implementali solo se il tuo backend espone questa funzionalità (tipicamente in dashboard di amministrazione storage). Quando assenti, eventuali UI che le richiedono devono fare feature-detect (if (typeof adapter.findOrphaned === 'function') { ... }).