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) removeupdate(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
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:

  1. Effect.ts — typed HTTP, dependency injection via Layers, streaming SSE, structured errors
  2. 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
npm
2026-04-01 01:29:03 +00:00
3
UNLICENSED
latest
126 KiB
Assets (1)
Versions (1) View all
0.36.0 2026-04-01