Concetti

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>
}
Tutti i metodi sono richiesti dal contratto. Per un mock di test puoi stub-arli con 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'
Deprecati: 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'