AttachmentAdapter
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 }) }
}
}
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') { ... }).