pgflex (0.36.0)
Published 2026-04-01 01:29:03 +00:00 by amir
Installation
registry=npm install pgflex@0.36.0"pgflex": "0.36.0"About this package
pgflex
Effect-based TypeScript client for PgFlex — schemaless PostgreSQL via PostgREST. Typed errors, Layer DI, streaming SSE, 55+ React hooks, 8 middleware, 952 tests.
Install
# Configure the private registry (one-time):
echo '@amir:registry=https://registry.spaceoperator.org/api/packages/amir/npm/' >> .npmrc
# Install:
npm install pgflex
# Peer dependencies (pick what you need):
npm install react @tanstack/react-query # React hooks
npm install zod react-hook-form @hookform/resolvers # Form integration
Quick Start
React
import { PgFlexProvider, useFind, useMutation } from "pgflex/react"
function App() {
return (
<PgFlexProvider url="http://localhost:3000" sseUrl="http://localhost:3001">
<TodoList />
</PgFlexProvider>
)
}
function TodoList() {
const { data: todos, isLoading } = useFind("todos", { status: "eq.active" })
const { insert, remove } = useMutation("todos")
if (isLoading) return <p>Loading...</p>
return (
<ul>
{todos?.map(todo => (
<li key={todo.id}>
{todo.title}
<button onClick={() => remove({ id: `eq.${todo.id}` })}>Delete</button>
</li>
))}
<button onClick={() => insert({ title: "New todo", status: "active" })}>
Add
</button>
</ul>
)
}
Effect (headless)
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "@effect/platform"
import { PgFlexClient, PgFlexConfig } from "pgflex"
const program = Effect.gen(function* () {
const client = yield* PgFlexClient
const todos = client.collection("todos")
yield* todos.insert({ title: "Buy milk", status: "active" })
const rows = yield* todos.find({ status: "eq.active" })
return rows
})
const MainLayer = PgFlexClient.Live.pipe(
Layer.provide(PgFlexConfig.live({ url: "http://localhost:3000" })),
Layer.provide(FetchHttpClient.layer),
)
Effect.runPromise(program.pipe(Effect.provide(MainLayer)))
Entry Points
| Import | Purpose |
|---|---|
pgflex |
Core: Client, Config, Collection, Filter, Schema, Errors, utilities |
pgflex/react |
55+ React hooks (requires react + @tanstack/react-query) |
pgflex/ssr |
Server-side rendering: createServerRuntime, prefetch helpers |
pgflex/testing |
createMockPgFlex() + assertion helpers for unit tests |
pgflex/middlewares |
Direct middleware imports (retry, tracing, circuit breaker, etc.) |
Hook Reference
Core Query
| Hook | Description |
|---|---|
useFind(collection, filter?, options?) |
Fetch rows with PostgREST filtering |
useFindOne(collection, filter, options?) |
Fetch a single row |
useCount(collection, filter?) |
Count matching rows |
useSubscribe(collection, filter?) |
useFind + SSE awareness |
useLazyFind(collection) |
Query on demand via execute() — not on mount |
useLazyFindOne(collection) |
Single-row query on demand |
useTypedFind(def, filter?) |
Schema-validated find with Effect Schema |
useSuspenseFind(collection, filter?) |
React Suspense-compatible find |
useSuspenseFindOne(collection, filter?) |
Suspense single-row |
useSuspenseRpc(name, params?) |
Suspense RPC call |
Core Mutation
| Hook | Description |
|---|---|
useMutation(collection) |
Insert, update, remove with optimistic cache updates |
useTypedMutation(def) |
Schema-validated mutations |
useUpsert(collection) |
Insert-or-update via PostgREST resolution=merge-duplicates |
useRpc(name) |
Call PostgREST RPC functions |
useAdmin() |
All 12 admin operations (bulk, schema, sync, computed fields) |
useTransaction(ops) |
Multi-collection sequential operations with rollback on failure |
Advanced Query
| Hook | Description |
|---|---|
usePipeline(collection, pipeline) |
Fluent where → sort → map → take client-side transforms |
useComputedFind(collection, fn) |
Derived/grouped data from cache (groupBy, sortBy, sumBy) |
useSelectiveFind(collection, columns) |
Fetch specific columns only (?select=col1,col2) |
useSelectiveFindOne(collection, columns, filter) |
Single-row selective fetch |
useJoinedFind(config) |
Client-side inner/left join of two collections |
useDependentQuery(config) |
Cascading queries — B's filter derived from A's result |
useRealtimeFind(collection, filter?) |
SSE-driven auto-invalidation with debounce |
useEmbed(collection, embed) |
PostgREST resource embedding (FK joins) |
useAggregate(collection, options) |
COUNT, SUM, AVG, GROUP BY via PostgREST |
useParallelQueries(descriptors) |
Multi-collection parallel fetch |
Advanced Mutation
| Hook | Description |
|---|---|
useOptimisticMutation(collection) |
Instant UI feedback with snapshot + rollback on error |
useRetryMutation(collection, config) |
Per-operation retry with exponential/fixed backoff |
useSerializedMutation(collection) |
FIFO queue preventing double-submit race conditions |
useValidatedMutation(collection, schema) |
Effect Schema validation before server call |
useSoftDelete(collection) |
remove → update(deleted_at), auto-filter deleted rows |
useOfflineMutation(collection) |
Queue mutations when offline, auto-replay on reconnect |
useOptimisticList(collection) |
Add/remove/reorder lists with instant feedback (parallel updates) |
useMutationHistory(collection) |
Undo/redo with inverse operation tracking |
Batch & Export
| Hook | Description |
|---|---|
useBatchInsert/Update/Remove(collection) |
Chunked batch operations with progress |
useBulkOperation(collection) |
Admin bulk import: chunked, progress, cancel, error collection |
useDataExport(collection) |
CSV/JSON/NDJSON download with column selection |
Pagination & Search
| Hook | Description |
|---|---|
usePagination(collection, filter?) |
Cursor-based keyset pagination |
useInfiniteFind(collection, filter?) |
Load-more / infinite scroll |
useDebouncedFind(collection, term) |
Search-as-you-type with configurable debounce |
usePolling(collection, interval) |
Interval-based refetch with visibility API |
useQueryState(collection) |
Sync filter/sort/page to URL params (bookmarkable views) |
Forms & Filters
| Hook | Description |
|---|---|
useCollectionForm(collection, schema) |
react-hook-form + Zod bridge with Effect Schema |
useEditForm(collection, id) |
Edit existing row: auto-fetch, pre-fill, submit |
useFilterBuilder() |
Dynamic filter panel: add/remove conditions, logical groups |
Cache Management
| Hook | Description |
|---|---|
useOptimisticUpdate(collection) |
Manual cache snapshot/append/update/remove/rollback |
usePrefetch(collection) |
Eager cache priming |
usePrefetchQuery(collection, filter?) |
Prefetch with staleTime |
useInvalidation(collection) |
Granular cache invalidation |
useCacheTags(tags) |
Group queries under string labels for bulk invalidation |
useTaggedFind(collection, tags) |
useFind + automatic cache tag registration |
useCascadeInvalidation(rules) |
Auto-invalidate related collections on mutation |
useCancellableQuery(queryFn) |
Manual AbortSignal cancel control |
useAbortableFind(collection) |
useFind with manual cancel() |
Infrastructure & Monitoring
| Hook | Description |
|---|---|
useConnectionStatus() |
SSE + API connection state |
useOnReconnect(callback) |
Fire callback when connection restores |
useHealth() |
PostgREST health check |
useColumnBudget() |
Column budget observability |
useMetrics() |
Live request stats (p50/p95/p99, error rate) |
useQueryLifecycle(collection) |
Fetch duration, render duration, stale detection |
usePerformanceMonitor(monitor) |
Threshold-based alerts with health score |
useErrorHandler() |
Centralized error formatting with history and auto-clear |
useRequestTrace(tracing) |
Correlation ID debugging |
useSchemaMetadata(collection) |
Runtime column/type/nullable discovery from OpenAPI |
Realtime & Collaboration
| Hook | Description |
|---|---|
usePresence(channel, data) |
User presence with heartbeat and stale sweep |
usePresenceList(channel) |
Read-only presence observer |
useShape(collection) |
ElectricSQL one-shot shape subscription |
useShapeLive(collection) |
ElectricSQL polling shape |
Components
| Component | Description |
|---|---|
PgFlexProvider |
Root provider: Effect runtime + QueryClient + SSE |
PgFlexDevTools |
Tabbed diagnostics: overview, mutation timeline, cache inspector, query waterfall. No-ops in production builds. |
PgFlexHydrationBoundary |
SSR hydration wrapper for React Query |
MockPgFlexProvider |
Test wrapper with in-memory mock store |
prefetchFind/FindOne/Rpc/TypedFind |
Server-side prefetch for SSR |
dehydratePgFlex |
Dehydrate query cache for SSR transfer |
Middleware
import { PgFlexConfig } from "pgflex"
import {
createRetryMiddleware,
createTracingMiddleware,
createCircuitBreakerMiddleware,
productionMiddleware,
} from "pgflex/middlewares"
// Use a preset:
PgFlexConfig.live({
url: "http://localhost:3000",
middleware: productionMiddleware(), // retry → circuit breaker → dedup → metrics
})
// Or compose your own:
PgFlexConfig.live({
url: "http://localhost:3000",
middleware: [
createTracingMiddleware({ onRequest: (id, req) => console.log(`[${id}] ${req.method} ${req.url}`) }),
createRetryMiddleware({ maxRetries: 3 }),
createCircuitBreakerMiddleware({ failureThreshold: 5 }),
],
})
| Middleware | Purpose |
|---|---|
createRetryMiddleware |
Exponential backoff on 429/5xx |
createCircuitBreakerMiddleware |
Closed → open → half-open state machine |
createDeduplicationMiddleware |
Collapse identical in-flight GET/HEAD |
createMetricsMiddleware |
Ring buffer with p50/p95/p99 latency |
createRateLimitMiddleware |
Client-side sliding window token bucket |
createAuthRefreshMiddleware |
401 retry with JWT refresh (shared promise) |
createLoggerMiddleware |
Request/response logging with filtering |
createTracingMiddleware |
X-Request-ID correlation headers, trace history |
productionMiddleware() |
Preset: retry → circuit breaker → dedup → metrics |
developmentMiddleware() |
Preset: logger → metrics → retry |
Error Handling
import { formatError, isPgFlexError } from "pgflex"
try {
await insert({ email: "duplicate@example.com" })
} catch (err) {
const formatted = formatError(err)
// { title: "Validation error",
// detail: "A record with this users email already exists.",
// severity: "warning",
// suggestion: "Please check your input and try again." }
}
Built-in detection for: unique constraints, FK violations, not-null, check constraints, 401/403/404/409/429/5xx.
useErrorHandler (React)
import { useErrorHandler } from "pgflex/react"
function MyForm() {
const { handleError, lastError, clearError, hasError } = useErrorHandler({
onError: (formatted, raw) => analytics.track("error", formatted),
autoClearMs: 5000,
})
const onSubmit = async (data) => {
try { await insert(data) }
catch (err) { handleError(err) }
}
return hasError ? <Alert severity={lastError.severity}>{lastError.detail}</Alert> : null
}
Performance Monitoring
import { createPerformanceMonitor } from "pgflex"
import { usePerformanceMonitor } from "pgflex/react"
const monitor = createPerformanceMonitor({
budgets: {
global: { warnMs: 1000, errorMs: 5000 },
users: { warnMs: 500, errorMs: 2000 },
},
onWarn: (v) => console.warn(`Slow: ${v.collection} ${v.durationMs}ms`),
onError: (v) => pagerduty.alert(`Critical: ${v.collection} ${v.durationMs}ms`),
})
// In React:
function HealthBadge() {
const { healthScore, violations, slowQueries } = usePerformanceMonitor(monitor)
return <Badge color={healthScore > 80 ? "green" : "red"}>{healthScore}%</Badge>
}
Testing
import { createMockPgFlex, expectInserted, expectRemoved } from "pgflex/testing"
import { MockPgFlexProvider } from "pgflex/react"
import { render } from "@testing-library/react"
const mock = createMockPgFlex({
todos: [
{ id: "1", created_at: "2024-01-01", data: {}, title: "Test todo" },
],
})
render(
<MockPgFlexProvider mock={mock}>
<YourComponent />
</MockPgFlexProvider>
)
// Assert mutations:
expect(mock.store("todos").inserted).toHaveLength(1)
expect(expectInserted(mock, "todos", { title: "New" })).toBe(true)
expect(expectRemoved(mock, "todos", { id: "eq.1" })).toBe(true)
// Reset between tests:
mock.resetAll()
The mock supports all PostgREST filter operators: eq, neq, gt, gte, lt, lte, like, is.null, not.is.null.
Typed Filters
import { filter } from "pgflex"
// Fluent type-safe filter builder
const f = filter<{ status: string; priority: number; title: string }>()
.eq("status", "active")
.gte("priority", 3)
.ilike("title", "*urgent*") // % → * conversion for URL safety
.or(
filter().eq("status", "review"),
filter().gt("priority", 5),
)
// Use with hooks:
const { data } = useFind("todos", f)
Soft Delete
import { useSoftDelete } from "pgflex/react"
function ArchiveableList() {
const { remove, restore, hardRemove, deletedCount } = useSoftDelete("todos", {
trackDeletedCount: true,
})
// remove() sets deleted_at instead of DELETE
// useFind() auto-excludes soft-deleted rows
// restore() clears deleted_at
// hardRemove() actually deletes
}
URL Query State
import { useQueryState, useFind } from "pgflex/react"
function FilterablePage() {
const qs = useQueryState("products", {
defaultSort: "created_at.desc",
defaultLimit: 25,
})
const { data } = useFind("products", qs.filter, {
order: qs.sort,
limit: qs.limit,
offset: qs.offset,
})
// URL updates automatically: ?f.status=eq.active&sort=name.asc&page=2
return (
<>
<button onClick={() => qs.setFilterField("status", "eq.active")}>Active</button>
<button onClick={() => qs.setSort("name.asc")}>Sort by name</button>
<Pagination page={qs.page} onChange={qs.setPage} />
</>
)
}
Architecture
Built on two layers:
- Effect.ts — typed HTTP, dependency injection via Layers, streaming SSE, structured errors
- React Query — caching, SWR, dedup, retry, Suspense, optimistic updates
PgFlexProvider
├── Effect ManagedRuntime (PgFlexClient + PgFlexConfig + HttpClient)
├── React Query QueryClient
└── SSE subscription → automatic cache invalidation
All React hooks use a shared effectBridge to convert Effect programs into Promises that React Query can manage — ensuring consistent AbortSignal forwarding and typed error handling.
Stats
- 55+ React hooks across 10 categories
- 31 core modules (Effect-based, framework-agnostic)
- 8 middleware + 2 presets
- 952 tests across 105 files
- 5 entry points (core, react, ssr, testing, middlewares)
- 127 kB package size (tests excluded from dist)
- 0 eslint-disable comments in production code
- 5 rounds of code review (v0.30–v0.35)
Dependencies
Dependencies
| ID | Version |
|---|---|
| @effect/platform | ^0.94.5 |
| effect | ^3.19.19 |
Development dependencies
| ID | Version |
|---|---|
| @hookform/resolvers | ^5.2.2 |
| @tanstack/react-query | ^5.90.21 |
| @testing-library/dom | ^10.4.1 |
| @testing-library/react | ^16.3.2 |
| @types/node | ^25.3.0 |
| @types/react | ^19.0.0 |
| happy-dom | ^20.8.3 |
| react | ^19.0.0 |
| react-hook-form | ^7.71.2 |
| typescript | ^5.4 |
| vitest | ^4.0.18 |
| zod | ^4.3.6 |
Peer dependencies
| ID | Version |
|---|---|
| @hookform/resolvers | ^5.0.0 |
| @tanstack/react-query | ^5.0.0 |
| react | ^18.0.0 || ^19.0.0 |
| react-hook-form | ^7.0.0 |
| zod | ^3.0.0 || ^4.0.0 |
Keywords
pgflex
postgresql
postgrest
schemaless
react
hooks
real-time
sse
Details
2026-04-01 01:29:03 +00:00
Assets (1)
Versions (1)
View all
npm
4
UNLICENSED
latest
126 KiB
pgflex-0.36.0.tgz
126 KiB
0.36.0
2026-04-01