Service Pattern (BasePostgrestService)
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.
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>
}
| Membro | Tipo | Descrizione |
|---|---|---|
repo | EnhancedPostgRESTRepository<T, K> | Repository sottostante, accessibile dalle classi figlie per query avanzate |
client | PostgRESTClient | Client facade, esposto per accesso a getHttpClient() (RPC custom) e cache |
entityName | string | Nome 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,
})
}
}
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
- Quick Start — Tre approcci a confronto
- Architettura — Layer DDD e dependency rule
- Installazione — Subpath
/mfeper variante microfrontend
Quick Start
Tre approcci per iniziare con @pzeta/postgrest - facade PostgRESTClient (semplice), setup manuale con EnhancedPostgRESTRepository (controllo), service pattern con BasePostgrestService (consigliato per progetti strutturati).
Facade & Services
Punto di ingresso applicativo della libreria — facade PostgRESTClient per setup rapido e BasePostgrestService come classe base per service di dominio CRUD + RPC.