IAttivitaRepository
title: IAttivitaRepository (Repository pattern) description: Contratto di dominio per l'accesso ai dati delle attività. Disaccoppia composables/componenti dal trasporto. PostgREST di default, sostituibile con qualsiasi implementazione.
Perché esiste
Senza un'astrazione, useAttivita e useAttivitaForm chiamerebbero direttamente endpoint PostgREST. Questo rende impossibile:
- Testare i composables senza un backend reale
- Usare la libreria con un backend non-PostgREST (REST custom, GraphQL, store locale)
- Sostituire l'implementazione per un singolo flusso (es. cache, batching)
IAttivitaRepository definisce il contratto di dominio delle operazioni. L'implementazione di default è PostgrestAttivitaRepository, ma puoi fornire la tua tramite l'opzione repository di AttivitaPlugin.
useAttivita / Wrapper components
│
▼
IAttivitaRepository ◄──────── contratto di dominio
▲
│
┌─────┴────────────────┬─────────────────────┐
│ │ │
PostgrestAttivitaRepository Mock repository GraphQL repository
(default, esportato) (test) (custom)
Interfaccia
L'interfaccia raggruppa le operazioni per area funzionale:
interface IAttivitaRepository {
// ── Lettura attività ──────────────────────────────────────────────────
getAttivita(filters: AttivitaFilters): Promise<AttivitaView[]>
getAttivitaById(idattivita: number): Promise<AttivitaView | null>
getAttivitaPerEntita(
entitasorgente: string,
identitasorgente: number,
filters?: Partial<AttivitaFilters>
): Promise<AttivitaView[]>
getAttivitaCalendario(start: string, end: string): Promise<AttivitaView[]>
// ── Riepilogo e contatori ─────────────────────────────────────────────
getRiepilogoEntita(
entitasorgente: string,
identitasorgente: number
): Promise<AttivitaRiepilogo | null>
getDashboardUtente(): Promise<AttivitaUtente | null>
contaAttivitaUtente(idutente?: number): Promise<ContatoreAttivita>
// ── Concatenamenti ────────────────────────────────────────────────────
createConcatenamento(data: { /* ... */ }): Promise<void>
deleteConcatenamento(idattivitasuccessiva: number): Promise<void>
// ── Tipi attività (admin) ─────────────────────────────────────────────
getTipiAttivita(soloAttivi?: boolean): Promise<TipoAttivita[]>
createTipoAttivita(data: Omit<TipoAttivita, 'idtipoattivita' | 'concatenamenti'>): Promise<TipoAttivita>
updateTipoAttivita(idtipoattivita: number, data: Partial<TipoAttivita>): Promise<TipoAttivita>
/** v1.0.0 — hard delete; lancia errore FK se ci sono attività referenziate */
deleteTipoAttivita(idtipoattivita: number): Promise<void>
// ── Assegnatari M:N ───────────────────────────────────────────────────
getAssegnatari(idattivita: number): Promise<AttivitaAssegnatario[]>
addAssegnatario(idattivita: number, idutente: number, principale?: boolean): Promise<void>
removeAssegnatario(idattivita: number, idutente: number): Promise<void>
// ── Partecipanti riunione ─────────────────────────────────────────────
getPartecipanti(idattivita: number): Promise<AttivitaPartecipante[]>
addPartecipante(idattivita: number, idutente: number): Promise<void>
updateStatoInvito(
idattivita: number,
idutente: number,
stato: 'accettato' | 'rifiutato'
): Promise<void>
// ── Ricorrenza ────────────────────────────────────────────────────────
getRicorrenza(idattivita: number): Promise<Ricorrenza | null>
createRicorrenza(data: /* ... */): Promise<Ricorrenza>
updateRicorrenza(idricorrenza: number, data: /* ... */): Promise<void>
disattivaRicorrenza(idricorrenza: number): Promise<void>
// ── RPC operazioni principali ─────────────────────────────────────────
pianificaAttivita(request: PianificaAttivitaRequest): Promise<PianificaAttivitaResponse>
completaAttivita(request: CompletaAttivitaRequest): Promise<CompletaAttivitaResponse>
annullaAttivita(request: AnnullaAttivitaRequest): Promise<AnnullaAttivitaResponse>
// ── Aggiornamenti diretti ─────────────────────────────────────────────
updateAttivita(idattivita: number, data: { /* campi principali */ }): Promise<void>
replaceAssegnatari(idattivita: number, idutenti: number[]): Promise<void>
ripristinaAttivita(idattivita: number): Promise<void>
eliminaAttivita(idattivita: number): Promise<void>
updateScadenza(idattivita: number, datascadenza: string): Promise<AttivitaView>
updateOrarioRiunione(idattivita: number, orarioinizio: string, orariofine: string): Promise<void>
}
vi.fn().mockResolvedValue(...). Vedi sezione "Mock per test" sotto.PostgrestAttivitaRepository — implementazione canonica
L'implementazione di default è esportata pubblicamente. Tre forme equivalenti per istanziarla:
import {
PostgrestAttivitaRepository,
createPostgrestRepository,
createFetchClient,
} from '@pzeta/vue-attivita'
const http = createFetchClient('/attivita', () => sessionStorage.getItem('jwt'))
// Forma 1: classe diretta
const repo1 = new PostgrestAttivitaRepository(http)
// Forma 2: factory
const repo2 = createPostgrestRepository(http)
Usata implicitamente dal plugin
Quando passi baseUrl al plugin (senza repository), PostgrestAttivitaRepository viene istanziato internamente:
app.use(AttivitaPlugin, {
baseUrl: '/api/attivita',
getToken: () => sessionStorage.getItem('jwt'),
})
// equivalente a:
// const http = createFetchClient('/api/attivita', () => sessionStorage.getItem('jwt'))
// const repository = new PostgrestAttivitaRepository(http)
// app.provide(ATTIVITA_CONTEXT_KEY, { repository, ... })
Repository custom
Esempio 1: GraphQL
import type {
IAttivitaRepository,
AttivitaView,
AttivitaFilters,
PianificaAttivitaRequest,
PianificaAttivitaResponse,
} from '@pzeta/vue-attivita'
import type { GraphQLClient } from 'graphql-request'
export class GraphQLAttivitaRepository implements IAttivitaRepository {
constructor(private readonly client: GraphQLClient) {}
async getAttivita(filters: AttivitaFilters): Promise<AttivitaView[]> {
const { attivita } = await this.client.request(LIST_ATTIVITA, filters)
return attivita
}
async getAttivitaPerEntita(entitasorgente, identitasorgente, filters) {
const { attivita } = await this.client.request(LIST_BY_ENTITA, {
entitasorgente, identitasorgente, ...filters,
})
return attivita
}
async pianificaAttivita(request: PianificaAttivitaRequest): Promise<PianificaAttivitaResponse> {
const { pianifica } = await this.client.request(PIANIFICA_MUTATION, request)
return pianifica
}
// ... resto delle 30+ firme
}
app.use(AttivitaPlugin, {
repository: new GraphQLAttivitaRepository(myGraphQLClient),
})
Esempio 2: estensione PostgREST con cache
import { PostgrestAttivitaRepository, type AttivitaView } from '@pzeta/vue-attivita'
class CachedAttivitaRepository extends PostgrestAttivitaRepository {
private cache = new Map<string, AttivitaView[]>()
async getAttivitaPerEntita(entitasorgente: string, identitasorgente: number, filters) {
const key = `${entitasorgente}:${identitasorgente}:${JSON.stringify(filters ?? {})}`
if (this.cache.has(key)) return this.cache.get(key)!
const result = await super.getAttivitaPerEntita(entitasorgente, identitasorgente, filters)
this.cache.set(key, result)
return result
}
async pianificaAttivita(request) {
const result = await super.pianificaAttivita(request)
this.cache.clear()
return result
}
}
Mock per test
Solo i metodi effettivamente usati dal componente in test devono essere stubbati. Per i restanti puoi castare a Partial<IAttivitaRepository> as IAttivitaRepository:
import { describe, it, expect, vi } from 'vitest'
import { ref } from 'vue'
import { mount } from '@vue/test-utils'
import { ATTIVITA_CONTEXT_KEY, AttivitaListaEntita } from '@pzeta/vue-attivita'
import type { IAttivitaRepository, AttivitaView } from '@pzeta/vue-attivita'
const fakeAttivita: AttivitaView[] = [/* ... */]
const mockRepository = {
getAttivitaPerEntita: vi.fn().mockResolvedValue(fakeAttivita),
getRiepilogoEntita: vi.fn().mockResolvedValue({
in_ritardo: 1, oggi: 0, futuro: 2, totale: 3,
}),
pianificaAttivita: vi.fn().mockResolvedValue({ idattivita: 99, tipo: 'telefonata' }),
// ... altri metodi necessari al test
} as unknown as IAttivitaRepository
it('mostra le attività dal repository', async () => {
const wrapper = mount(AttivitaListaEntita, {
props: { entitasorgente: 'anagrafica', identitasorgente: 42 },
global: {
provide: {
[ATTIVITA_CONTEXT_KEY as symbol]: {
repository: mockRepository,
service: mockRepository, // alias deprecato
getToken: () => null,
onEvent: () => {},
apiBaseUrl: '',
refreshSignal: ref(0), // v1.0.0
},
},
},
})
await wrapper.vm.$nextTick()
expect(mockRepository.getAttivitaPerEntita).toHaveBeenCalledWith('anagrafica', 42, undefined)
})
Iniezione del repository custom
import { AttivitaPlugin } from '@pzeta/vue-attivita'
app.use(AttivitaPlugin, {
repository: new MyCustomRepository(),
onEvent: (name, payload) => eventBus.emit(name, payload),
})
Quando passi repository, le opzioni baseUrl e http vengono ignorate — nessun client PostgREST viene istanziato.
Backward-compat: AttivitaService
Per compatibilità con codice esistente, il package esporta ancora:
import { AttivitaService, createAttivitaService } from '@pzeta/vue-attivita'
AttivitaService è un alias di IAttivitaRepository e createAttivitaService(http) è alias di new PostgrestAttivitaRepository(http). Saranno rimossi nella prossima major. Migra ai nomi canonici.TypeScript
import type { IAttivitaRepository } from '@pzeta/vue-attivita'
import {
PostgrestAttivitaRepository,
createPostgrestRepository,
createFetchClient,
} from '@pzeta/vue-attivita'