Getting Started

Service Pattern (BasePostgrestService)

Classe base astratta per service applicativi PostgREST. CRUD omogeneo, error messages italiani, RPC helpers, integrazione Inversify DI. Pattern di adozione canonico nei progetti PZeta.

Cos'è BasePostgrestService

BasePostgrestService<T, K> è una classe astratta che wrappa EnhancedPostgRESTRepository esponendo una API CRUD omogenea con error messages user-facing in italiano e utility per chiamate RPC PostgREST. È il pattern di adozione canonico nei progetti PZeta che integrano @pzeta/postgrest: promosso da vue-magazzino, ora vive nella libreria stessa per garantire uniformità tra repository.

Quando usarlo: in qualsiasi progetto strutturato che abbia più di un'entità di dominio. Ogni entità ha il proprio service che estende BasePostgrestService, registrabile in un container DI. Per la variante microfrontend basata su MFEHttpClient vedi BaseMfePostgrestService in @pzeta/postgrest/mfe.

API esposta

abstract class BasePostgrestService<
  T extends Record<string, unknown>,
  K extends string | number = number,
> {
  protected readonly repo: EnhancedPostgRESTRepository<T, K>
  protected readonly client: PostgRESTClient
  protected readonly entityName: string

  constructor(
    client: PostgRESTClient,
    endpoint: string,
    primaryKey: keyof T & string,
    entityName?: string, // default: 'Record'
  )

  getAll(filters?: FilterCriteria<T>): Promise<T[]>
  getById(id: K): Promise<T>                    // throw se non trovato
  create(data: Partial<T>): Promise<T>          // throw se fallisce
  update(id: K, data: Partial<T>): Promise<T>   // throw se fallisce
  delete(id: K): Promise<void>

  protected callRpc<R>(rpcPath: string, params: Record<string, unknown>): Promise<R>
}
MembroTipoDescrizione
repoEnhancedPostgRESTRepository<T, K>Repository sottostante, accessibile dalle classi figlie per query avanzate
clientPostgRESTClientClient facade, esposto per accesso a getHttpClient() (RPC custom) e cache
entityNamestringNome user-facing usato negli error messages (es. "Cliente non trovato: 42")

Esempio completo

import { BasePostgrestService, type PostgRESTClient } from '@pzeta/postgrest'

interface Cliente {
  idcliente: number
  ragionesociale: string
  codicefiscale: string
  email: string
  datacreazione: string
}

class ClientiService extends BasePostgrestService<Cliente, number> {
  constructor(client: PostgRESTClient) {
    super(client, '/clienti', 'idcliente', 'Cliente')
  }

  // Metodi di dominio: usano this.getAll() / this.repo / this.callRpc()
  async cercaPerRagioneSociale(ragione: string): Promise<Cliente[]> {
    return this.getAll({
      ragionesociale: { operator: 'ilike', value: `%${ragione}%` },
      sort: { field: 'ragionesociale', direction: 'asc' },
    })
  }

  async cercaPerCodiceFiscale(cf: string): Promise<Cliente | null> {
    const result = await this.getAll({
      codicefiscale: { operator: 'eq', value: cf },
    })
    return result[0] ?? null
  }

  // RPC PostgREST con parametri prefisso p_ (convenzione)
  async aggiornaSaldo(idcliente: number, importo: number): Promise<number> {
    return this.callRpc<number>('/rpc/aggiorna_saldo_cliente', {
      p_idcliente: idcliente,
      p_importo: importo,
    })
  }
}

Uso

import { PostgRESTClient } from '@pzeta/postgrest'

const client = new PostgRESTClient({ apiUrl: 'https://api.example.com' })
await client.init()
await client.login({ email: '...', password: '...' })

const clientiService = new ClientiService(client)

// CRUD standard
const elenco = await clientiService.getAll()
const cliente = await clientiService.getById(42)              // throw se non trovato
const nuovo = await clientiService.create({ ragionesociale: 'Acme' })
await clientiService.update(42, { email: 'info@acme.it' })
await clientiService.delete(42)

// Metodi di dominio
const acme = await clientiService.cercaPerCodiceFiscale('ACMEFISCAL')
const ricerca = await clientiService.cercaPerRagioneSociale('acme')
const saldo = await clientiService.aggiornaSaldo(42, 1500.00)

Error messages

I metodi getById, create, update lanciano errori con messaggi user-facing che usano entityName:

class FattureService extends BasePostgrestService<Fattura, number> {
  constructor(client: PostgRESTClient) {
    super(client, '/fatture', 'idfattura', 'Fattura')
  }
}

// Errori generati automaticamente
try {
  await fattureService.getById(999) // throw Error: "Fattura non trovato: 999"
} catch (e) { /* ... */ }

try {
  await fattureService.create({}) // throw Error: "Impossibile creare fattura"
} catch (e) { /* ... */ }

try {
  await fattureService.update(1, {}) // throw Error: "Impossibile aggiornare fattura: 1"
} catch (e) { /* ... */ }

Se non passi entityName, il default è 'Record'.

Override dei metodi

Le classi figlie possono fare override dei metodi base per aggiungere validation, logging o trasformazioni:

class FattureService extends BasePostgrestService<Fattura, number> {
  constructor(client: PostgRESTClient) {
    super(client, '/fatture', 'idfattura', 'Fattura')
  }

  // Override: aggiunge sort di default e logging
  override async getAll(filters?: FilterCriteria<Fattura>): Promise<Fattura[]> {
    console.log('[FattureService] getAll', filters)
    return super.getAll({
      sort: { field: 'datadocumento', direction: 'desc' },
      ...filters,
    })
  }

  // Override: validazione business pre-create
  override async create(data: Partial<Fattura>): Promise<Fattura> {
    if (!data.numerodocumento) {
      throw new Error('Numero documento obbligatorio')
    }
    return super.create(data)
  }
}

Helper RPC

callRpc<R>() è un metodo protetto pensato per essere esposto da metodi pubblici tipizzati nelle classi figlie. Convenzione PostgREST: i parametri delle stored procedure hanno prefisso p_.

class MagazzinoService extends BasePostgrestService<Articolo, number> {
  constructor(client: PostgRESTClient) {
    super(client, '/articoli', 'idarticolo', 'Articolo')
  }

  async ricalcolaGiacenze(idmagazzino: number): Promise<{ aggiornati: number }> {
    return this.callRpc('/rpc/ricalcola_giacenze', {
      p_idmagazzino: idmagazzino,
    })
  }

  async esportaListino(formato: 'csv' | 'xlsx'): Promise<string> {
    return this.callRpc<string>('/rpc/esporta_listino', {
      p_formato: formato,
    })
  }
}
Per RPC che ritornano array paginati, usa this.repo.callRpcPaginated() (esposto da EnhancedPostgRESTRepository) invece di callRpc(), in modo da gestire Range + Content-Range header.

Integrazione con Inversify DI

Pattern standard nei microservizi e frontend strutturati PZeta:

import { injectable, inject } from 'inversify'
import { BasePostgrestService, type PostgRESTClient } from '@pzeta/postgrest'
import { TYPES } from '@/di/types'

@injectable()
export class ClientiService extends BasePostgrestService<Cliente, number> {
  constructor(@inject(TYPES.PostgRESTClient) client: PostgRESTClient) {
    super(client, '/clienti', 'idcliente', 'Cliente')
  }

  async cercaPerRagioneSociale(ragione: string): Promise<Cliente[]> {
    return this.getAll({
      ragionesociale: { operator: 'ilike', value: `%${ragione}%` },
    })
  }
}

Container bindings

import { Container } from 'inversify'
import { PostgRESTClient } from '@pzeta/postgrest'
import { ClientiService } from '@/services/ClientiService'
import { FattureService } from '@/services/FattureService'

const container = new Container()

// Singleton: un solo client per applicazione
container.bind<PostgRESTClient>(TYPES.PostgRESTClient).toDynamicValue(async () => {
  const client = new PostgRESTClient({
    apiUrl: import.meta.env.VITE_APP_API_URL,
  })
  await client.init()
  return client
}).inSingletonScope()

// Service di dominio
container.bind<ClientiService>(TYPES.ClientiService).to(ClientiService).inSingletonScope()
container.bind<FattureService>(TYPES.FattureService).to(FattureService).inSingletonScope()

Vue composable wrapper

// composables/useClientiService.ts
import { inject } from 'vue'
import type { ClientiService } from '@/services/ClientiService'
import { CONTAINER_KEY, TYPES } from '@/di/keys'

export function useClientiService(): ClientiService {
  const container = inject(CONTAINER_KEY)
  if (!container) throw new Error('Container DI non disponibile')
  return container.get<ClientiService>(TYPES.ClientiService)
}

Pattern multi-progetto

Tutti i progetti PZeta che integrano @pzeta/postgrest seguono lo stesso layout:

src/
├── domain/
│   └── entities/         # Cliente.ts, Fattura.ts, ... (solo type)
├── application/
│   └── services/         # ClientiService.ts extends BasePostgrestService
├── infrastructure/
│   └── di/               # container.ts, types.ts
└── presentation/
    └── views/            # componenti Vue che usano i service via composable

Questo garantisce che il salto di repo a repo sia minimo: l'entità cambia, il service cambia, il pattern resta identico.

Prossimi passi