Repositories

PostgRESTRepository

Repository CRUD core con supporto completo PostgREST — CRUD, bulk, RPC, paginazione cursor-based, caching, multi-schema e prefetch proattivo.

Overview

PostgRESTRepository è l'implementazione concreta di ICrudRepository che parla HTTP verso un'istanza PostgREST. Estende CrudRepositoryBase e aggiunge:

  • Operazioni RPC (GET e POST) verso rpc/<funzione>
  • Caching integrato con CacheService e background refresh
  • Paginazione limit/offset e cursor-based con header Prefer: count=exact
  • Supporto multi-schema PostgreSQL via header Accept-Profile / Content-Profile
  • Operazioni bulk: createMany, updateMany, deleteMany, deleteManyByFilter
  • Prefetch proattivo (prefetchRecord, prefetchRecords)

Path multipli sulla stessa istanza: invocato con path diversi su ogni chiamata.

Costruttore

new PostgRESTRepository(
  baseUrl: string,
  cacheTTL?: number,
  _useBackgroundRefresh = true,
  isAuthRequired = false,
  useCache = false,
  authHandler?: IAuthHandler,
  schema?: string
)
ParametroTipoDefaultDescrizione
baseUrlstringURL base dell'API PostgREST (es. https://api.example.com)
cacheTTLnumber?undefinedTTL cache in millisecondi
_useBackgroundRefreshbooleantrueRefresh cache in background (stale-while-revalidate)
isAuthRequiredbooleanfalseSe true, le richieste includono token via authHandler
useCachebooleanfalseAbilita caching delle risposte read
authHandlerIAuthHandler?Handler di autenticazione (vedi AuthStrategy)
schemastring?'public'Schema PostgreSQL da targettare

API CRUD

MetodoSignatureNote
createcreate<T>(path, element, _keyName?): Promise<T | undefined>POST con Prefer: return=representation
createOrUpdatecreateOrUpdate<T>(path, element, keyName): Promise<T | undefined>UPSERT via on_conflict
readread<T, K>(path, key, keyName, forceRefresh?): Promise<T | undefined>GET singolo, Accept: vnd.pgrst.object+json
readAllreadAll<T>(path, filters?, forceRefresh?): Promise<T[] | PaginatedResponse<T> | undefined>Polimorfico: array o paginato in base ai filtri
readPaginatedreadPaginated<T>(path, filters?, forceRefresh?): Promise<PaginatedResponse<T> | undefined>Sempre risposta paginata (default limit: 20)
updateupdate<T, K>(path, key, element, keyName): Promise<T | undefined>PATCH parziale
deletedelete<T, K>(path, key, keyName): Promise<T | undefined>DELETE con representation
createManycreateMany<T>(path, elements): Promise<T[] | undefined>POST batch atomico
updateManyupdateMany<T>(path, element, filters?): Promise<T[] | undefined>PATCH bulk filtrato
deleteManydeleteMany<T, K>(path, keys, keyName): Promise<T[] | undefined>DELETE via IN (...)
deleteManyByFilterdeleteManyByFilter<T>(path, filters): Promise<T[] | undefined>DELETE su criteri
getNextPagegetNextPage<T>(path, cursor, limit?, filters?): Promise<PaginatedResponse<T> | undefined>Paginazione cursor-based

API RPC

MetodoSignatureTrasporto
rpcGetrpcGet<T>(path, params?): Promise<T[] | undefined>GET rpc/<funzione>?param=...
rpcRawGetrpcRawGet<X>(path, params?, cached?): Promise<X>GET con tipo di ritorno generico
rpcPostrpcPost<X>(path, params?): Promise<X>POST body JSON
rpcRawPostrpcRawPost<X>(path, params?): Promise<X>POST mai cachato

Gestione cache

MetodoDescrizione
invalidateCache<K>(path, key)Invalida cache di un singolo record
invalidateAllCache(path)Invalida tutta la cache per la tabella (read/ + readAll/)
getCount(path, filters?)Restituisce solo il conteggio totale (header Content-Range)
prefetchRecord(path, key, keyName)Precarica un record in background (best-effort)
prefetchRecords<T>(path, filters?)Precarica una lista in background

FilterCriteria

I metodi readAll, readPaginated, updateMany, deleteManyByFilter accettano un oggetto FilterCriteria<T> con operatori PostgREST nativi.

interface FilterCondition {
  operator: 'eq' | 'neq' | 'gt' | 'lt' | 'gte' | 'lte' |
            'in' | 'like' | 'ilike' | 'is' | 'not' |
            'between' | 'cs' | 'cd' |
            'fts' | 'plfts' | 'phfts' |
            'sl' | 'sr' | 'nxl' | 'nxr' | 'adj'
  value: FilterValue | FilterValue[]
}

interface FilterCriteria<T> {
  [field: keyof T]: FilterCondition
  sort?: { field: keyof T; direction: 'asc' | 'desc' }
  pagination?: { limit?: number; offset?: number; cursor?: string }
}

Esempi rapidi:

// Uguaglianza
{ status: { operator: 'eq', value: 'active' } }

// Range numerico
{ price: { operator: 'between', value: [10, 100] } }

// Full-text search
{ content: { operator: 'fts', value: 'typescript' } }

// IN list
{ idruolo: { operator: 'in', value: [1, 2, 3] } }

// Pattern matching case-insensitive
{ email: { operator: 'ilike', value: '*@gmail.com' } }

Dettagli completi: FilterCriteria.

Paginazione

Due modalità di paginazione, entrambe restituiscono PaginatedResponse<T>:

interface PaginatedResponse<T> {
  data: T[]
  pagination: {
    totalCount?: number
    hasMore: boolean
    nextCursor?: string
  }
}

Offset-based (classica)

const page = await repo.readPaginated<User>('/users', {
  pagination: { limit: 20, offset: 40 },
  sort: { field: 'datacreazione', direction: 'desc' },
})

console.log(`Totale: ${page?.pagination.totalCount}`)

L'header Prefer: count=exact viene aggiunto automaticamente quando pagination è presente, popolando totalCount dall'header Content-Range.

Cursor-based (infinite scroll)

let cursor: string | undefined
do {
  const page = cursor
    ? await repo.getNextPage<User>('/users', cursor, 50)
    : await repo.readPaginated<User>('/users', { pagination: { limit: 50 } })

  for (const u of page?.data ?? []) renderUser(u)
  cursor = page?.pagination.nextCursor
} while (cursor)

RPC — chiamare funzioni PostgreSQL

PostgREST espone le funzioni PostgreSQL sotto /rpc/<nome_funzione>. Usa rpcGet per funzioni read-only, rpcPost per funzioni che modificano dati o accettano payload complessi.

// SELECT * FROM get_active_users(min_age => 18)
const users = await repo.rpcGet<User>('get_active_users', { min_age: 18 })

// Funzione che restituisce un primitivo
const count = await repo.rpcRawGet<number>('count_active_users', {}, true)

// Funzione di scrittura
const result = await repo.rpcPost<{ ok: boolean }>('process_batch', { batchId: 123 })
rpcPost non è mai cachato (cacheKey: 'no'). Se hai bisogno di caching su una funzione read-only complessa con payload nel body, valuta rpcRawGet con cached: true.

Esempio completo type-safe

import { PostgRESTRepository } from '@pzeta/postgrest'
import type { FilterCriteria } from '@pzeta/postgrest'

interface User extends Record<string, unknown> {
  idutente: number
  nome: string
  email: string
  status: 'active' | 'inactive'
  datacreazione: string
}

const repo = new PostgRESTRepository(
  'https://api.example.com',
  3_600_000,           // cache TTL 1 ora
  true,                // background refresh
  true,                // auth required
  true,                // use cache
  authHandler,
  'app',               // schema PostgreSQL
)

// Create
const nuovo = await repo.create<User>('/utenti', {
  nome: 'Mario Rossi',
  email: 'mario@example.com',
  status: 'active',
})

// Read singolo
const utente = await repo.read<User, number>('/utenti', 1, 'idutente')

// ReadAll con filtri
const attivi = await repo.readAll<User>('/utenti', {
  status: { operator: 'eq', value: 'active' },
  sort: { field: 'datacreazione', direction: 'desc' },
})

// ReadPaginated
const pagina = await repo.readPaginated<User>('/utenti', {
  pagination: { limit: 20, offset: 0 },
})

// Update
await repo.update<User, number>('/utenti', 1, { nome: 'Mario R.' }, 'idutente')

// Delete bulk
await repo.deleteMany<User, number>('/utenti', [5, 12, 23], 'idutente')

// RPC
const stats = await repo.rpcRawGet<DashboardStats>('get_dashboard_stats')

Cross-reference