Module to connect to the data store (postgres via postgrest, supabase, etc)
npm install hazo_connectA fully independent database abstraction layer with query builder interface for Node.js and Next.js applications. Supports multiple database backends including PostgreSQL (via PostgREST), SQLite, and Supabase.
- Features
- Installation
- Quick Start
- Entry Points
- Usage Examples
- Basic Query Builder
- CRUD Operations
- CRUD Service Helper
- Singleton Pattern
- Next.js API Routes
- Configuration
- SQLite
- PostgREST
- Supabase
- Environment Variables
- Key Interfaces
- Services
- SQLite Admin UI
- Supported Operators
- Server-Side Enforcement
- Documentation
---
- Query Builder API: Fluent interface supporting full PostgREST syntax
- Multiple Adapters: Support for PostgREST, Supabase, SQLite, and file storage
- Server-Side Only: Enforced server-side usage with runtime guards and Next.js 'use server' directives
- Zero Dependencies: Core component has no external dependencies (only Node.js built-ins)
- Dependency Injection: Logger and configuration injected, not imported
- Type Safe: Full TypeScript support with comprehensive type definitions
- SQLite Admin UI: Built-in admin interface for browsing and managing SQLite databases
- Singleton Pattern: Built-in support for connection pooling across API routes
---
``bash`
npm install hazo_connect
If you're using the SQLite admin UI, install these peer dependencies:
`bash`
npm install next@>=14.0.0 react@>=18.0.0 react-dom@>=18.0.0 lucide-react@^0.553.0 sonner@^2.0.7
Note: The core library (database adapters and query builder) has no peer dependencies.
---
`bash`.env.local
HAZO_CONNECT_TYPE=sqlite
HAZO_CONNECT_SQLITE_PATH=./database.sqlite
HAZO_CONNECT_ENABLE_ADMIN_UI=true
`typescript
import { createHazoConnectFromEnv } from 'hazo_connect/nextjs/setup'
import { QueryBuilder } from 'hazo_connect/server'
import { NextResponse } from 'next/server'
export async function GET() {
const hazo = createHazoConnectFromEnv()
const users = await hazo.query(new QueryBuilder().from('users'))
return NextResponse.json({ data: users })
}
`
---
hazo_connect provides multiple entry points for different use cases:
| Entry Point | Purpose | Use Case |
|-------------|---------|----------|
| hazo_connect | Types only | Client components (type imports) |hazo_connect/server
| | Server-side functionality | API routes, Server Components |hazo_connect/nextjs
| | Next.js helpers | API route handlers |hazo_connect/nextjs/setup
| | Setup utilities | Environment-based config, singleton pattern |hazo_connect/ui
| | UI-safe types | Client component type imports |
`typescript
// Server-side code (API routes, Server Components)
import { createHazoConnect, QueryBuilder, createCrudService } from 'hazo_connect/server'
// Next.js setup helpers
import { createHazoConnectFromEnv, getHazoConnectSingleton } from 'hazo_connect/nextjs/setup'
// API route handlers
import { createApiRouteHandler, getServerHazoConnect } from 'hazo_connect/nextjs'
// Client component types only
import type { HazoConnectConfig, TableSummary } from 'hazo_connect/ui'
`
---
`typescript
import { createHazoConnect, QueryBuilder } from 'hazo_connect/server'
const hazo = createHazoConnect({
type: 'sqlite',
sqlite: { database_path: './database.sqlite' }
})
// Simple select
const users = await hazo.query(
new QueryBuilder()
.from('users')
.select(['id', 'name', 'email'])
)
// With filters
const user = await hazo.query(
new QueryBuilder()
.from('users')
.where('id', 'eq', '123')
)
// With ordering and pagination
const paginatedUsers = await hazo.query(
new QueryBuilder()
.from('users')
.order('created_at', 'desc')
.limit(10)
.offset(20)
)
// Multiple conditions
const activeAdmins = await hazo.query(
new QueryBuilder()
.from('users')
.where('status', 'eq', 'active')
.where('role', 'eq', 'admin')
)
// Nested selects (PostgREST)
const pagesWithImages = await hazo.query(
new QueryBuilder()
.from('template_pages')
.nestedSelect('images', ['id', 'filename'])
)
`
`typescript
// CREATE - Insert a record
const newUser = await hazo.query(
new QueryBuilder().from('users'),
'POST',
{ name: 'John Doe', email: 'john@example.com' }
)
// READ - Select records
const users = await hazo.query(
new QueryBuilder().from('users').where('status', 'eq', 'active')
)
// UPDATE - Uses 'PATCH' method (not 'update')
const updated = await hazo.query(
new QueryBuilder()
.from('users')
.where('id', 'eq', '123'),
'PATCH',
{ name: 'Jane Doe', email: 'jane@example.com' }
)
// DELETE - Uses 'DELETE' method (not 'delete')
await hazo.query(
new QueryBuilder()
.from('users')
.where('id', 'eq', '123'),
'DELETE'
)
`
Important:
- Updates use PATCH method, not update()DELETE
- Deletes use method, not delete().where()
- Always use to specify which rows to update/delete
For simpler CRUD operations, use the createCrudService helper:
`typescript
import { createHazoConnect, createCrudService } from 'hazo_connect/server'
const hazo = createHazoConnect({ type: 'sqlite', sqlite: { database_path: './db.sqlite' } })
const userService = createCrudService(hazo, 'users')
// List all records
const allUsers = await userService.list()
// Find by ID
const user = await userService.findById('123')
// Find by criteria
const admins = await userService.findBy({ role: 'admin' })
// Insert
const newUser = await userService.insert({ name: 'John', email: 'john@example.com' })
// Update by ID - method is updateById() (not update())
await userService.updateById('123', { name: 'Jane' })
// Delete by ID - method is deleteById() (not delete())
await userService.deleteById('123')
// Custom query
const customQuery = userService.query()
.where('status', 'eq', 'active')
.order('created_at', 'desc')
.limit(5)
const result = await customQuery.execute('GET')
`
#### Auto-UUID Generation
The createCrudService function automatically generates UUIDs for TEXT primary key columns when inserting records. This feature is particularly useful for SQLite databases where TEXT PRIMARY KEY columns don't auto-generate IDs.
Default Behavior:
- Auto-ID generation is enabled by default for the 'id' column
- Uses Node.js built-in crypto.randomUUID()
- Preserves existing IDs when provided
- Only generates UUIDs when ID is missing (null or undefined)
Configuration Examples:
`typescript
// Default - auto-generates UUIDs for 'id' column (enabled by default)
const userService = createCrudService(hazo, 'users')
// Insert without ID - UUID will be auto-generated
await userService.insert({ name: 'John', email: 'john@example.com' })
// Result: { id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', name: 'John', ... }
// Insert with ID - provided ID is preserved
await userService.insert({ id: 'custom-id', name: 'Jane' })
// Result: { id: 'custom-id', name: 'Jane' }
// Opt-out for tables with auto-increment integer IDs
const logService = createCrudService(hazo, 'logs', { autoId: false })
// Custom ID column name
const itemService = createCrudService(hazo, 'items', {
autoId: { enabled: true, column: 'item_id' }
})
`
AutoIdConfig Interface:
`typescript`
interface AutoIdConfig {
enabled: boolean // Enable/disable auto-ID generation
column?: string // ID column name (default: 'id')
type?: 'uuid' // ID type (currently only UUID supported)
}
For multiple API routes sharing a database connection:
`typescript
// lib/hazo_connect.ts
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup'
export const hazo = getHazoConnectSingleton({
enableAdminUi: true
})
`
`typescript
// app/api/users/route.ts
import { hazo } from '@/lib/hazo_connect'
import { QueryBuilder } from 'hazo_connect/server'
import { NextResponse } from 'next/server'
export async function GET() {
const users = await hazo.query(new QueryBuilder().from('users'))
return NextResponse.json({ data: users })
}
`
#### Using createApiRouteHandler
`typescript
import { createApiRouteHandler } from 'hazo_connect/nextjs'
import { QueryBuilder } from 'hazo_connect/server'
import { NextResponse } from 'next/server'
export const GET = createApiRouteHandler(
async (hazo, request) => {
const users = await hazo.query(
new QueryBuilder().from('users').select('*')
)
return NextResponse.json({ data: users })
},
{
config: {
type: 'sqlite',
sqlite: { database_path: './database.sqlite' }
}
}
)
`
#### Using getServerHazoConnect
`typescript
import { getServerHazoConnect } from 'hazo_connect/nextjs'
import { QueryBuilder } from 'hazo_connect/server'
import { NextResponse } from 'next/server'
export async function GET() {
const hazo = getServerHazoConnect({
type: 'sqlite',
sqlite: { database_path: './database.sqlite' }
})
const users = await hazo.query(
new QueryBuilder().from('users').select('*')
)
return NextResponse.json({ data: users })
}
`
---
`typescript
import { createHazoConnect } from 'hazo_connect/server'
const hazo = createHazoConnect({
type: 'sqlite',
enable_admin_ui: true, // Enable admin UI (default: false)
sqlite: {
database_path: '/path/to/database.db', // Optional - in-memory when omitted
read_only: false, // Prevent writes when true
initial_sql: [ // Optional seed statements
CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);`
],
wasm_directory: '/path/to/sql.js/dist' // Optional WASM override
}
})
`typescript`
const hazo = createHazoConnect({
type: 'postgrest',
postgrest: {
base_url: 'http://localhost:3000',
api_key: 'your-api-key'
}
})
`typescript`
const hazo = createHazoConnect({
type: 'supabase',
supabase: {
url: 'https://your-project.supabase.co',
anon_key: 'your-anon-key',
service_role_key: 'your-service-role-key' // Optional
}
})
| Variable | Description | Default |
|----------|-------------|---------|
| HAZO_CONNECT_TYPE | Database type (sqlite, postgrest, supabase) | sqlite |HAZO_CONNECT_SQLITE_PATH
| | Path to SQLite database file | ./database.sqlite |HAZO_CONNECT_SQLITE_READONLY
| | Enable read-only mode | false |HAZO_CONNECT_SQLITE_WASM_DIR
| | Path to sql-wasm.wasm directory | Auto-detected |HAZO_CONNECT_ENABLE_ADMIN_UI
| | Enable SQLite admin UI | false |POSTGREST_URL
| | PostgREST base URL | - |POSTGREST_API_KEY
| | PostgREST API key | - |SUPABASE_URL
| | Supabase project URL | - |SUPABASE_ANON_KEY
| | Supabase anonymous key (respects RLS) | - |SUPABASE_SERVICE_ROLE_KEY
| | Supabase service role key (bypasses RLS) | - |NEXT_PUBLIC_SUPABASE_URL
| | Alternative Supabase URL (for Next.js) | - |NEXT_PUBLIC_SUPABASE_ANON_KEY
| | Alternative Supabase anon key (for Next.js) | - |HAZO_CONNECT_LOG_LEVEL
| | Log level (debug, info, warn, error, none) | warn (prod), info (dev) |
Control the verbosity of hazo_connect logs. Useful for reducing noise from frequent operations (e.g., polling) or debugging issues.
Log Levels:
- debug - Log all operations including query details (verbose)info
- - Log general operations and successful requests (default in development)warn
- - Log warnings and errors only (default in production)error
- - Log errors onlynone
- - Disable all logging
Configuration via environment variable:
`bash`
HAZO_CONNECT_LOG_LEVEL=warn
Configuration via config object:
`typescript`
const hazo = createHazoConnect({
type: 'postgrest',
log_level: 'warn', // Only log warnings and errors
postgrest: {
base_url: process.env.POSTGREST_URL,
api_key: process.env.POSTGREST_API_KEY
}
})
Completely silent:
`typescript
import { noOpLogger } from 'hazo_connect/server'
const hazo = createHazoConnect({
type: 'postgrest',
logger: noOpLogger, // No logs at all
postgrest: { base_url: '...', api_key: '...' }
})
`
See docs/logging.md for detailed configuration options.
---
Main configuration interface:
`typescript`
interface HazoConnectConfig {
type: 'postgrest' | 'supabase' | 'sqlite' | 'file'
logger?: Logger
log_level?: 'debug' | 'info' | 'warn' | 'error' | 'none'
enable_admin_ui?: boolean // SQLite only
postgrest?: { base_url: string; api_key: string }
supabase?: { url: string; anon_key: string; service_role_key?: string }
sqlite?: { database_path?: string; read_only?: boolean; initial_sql?: string[]; wasm_directory?: string }
file?: { base_path: string; file_format: string }
}
Database adapter interface:
`typescript`
interface HazoConnectAdapter {
query(builder: QueryBuilder, method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', body?: any): Promise
rawQuery(endpoint: string, options?: RequestInit): Promise
getConfig(): Promise
}
Optional logger interface:
`typescript`
interface Logger {
debug(message: string, data?: Record
info(message: string, data?: Record
warn(message: string, data?: Record
error(message: string, data?: Record
}
CRUD service interface:
`typescript
interface CrudService
list(configure?: (qb: QueryBuilder) => QueryBuilder): Promise
findBy(criteria: Record
findOneBy(criteria: Record
findById(id: unknown): Promise
insert(data: Partial
updateById(id: unknown, patch: Partial
deleteById(id: unknown): Promise
query(): ExecutableQueryBuilder
}
interface CrudServiceOptions {
primaryKeys?: string[] // defaults to ['id']
logger?: Logger
autoId?: AutoIdConfig | false // Auto-generate IDs on insert (enabled by default)
}
interface AutoIdConfig {
enabled: boolean
column?: string // Default: 'id'
type?: 'uuid' // Currently only UUID supported
}
`
---
Provides admin functionality for SQLite databases:
`typescript
import { getSqliteAdminService } from 'hazo_connect/server'
const adminService = getSqliteAdminService()
// List all tables
const tables = await adminService.listTables()
// Get table schema
const schema = await adminService.getTableSchema('users')
// Get table data with pagination and filtering
const data = await adminService.getTableData('users', {
limit: 20,
offset: 0,
order_by: 'created_at',
order_direction: 'desc',
filters: [{ column: 'status', operator: 'eq', value: 'active' }]
})
// Insert row
await adminService.insertRow('users', { name: 'John', email: 'john@example.com' })
// Update rows
await adminService.updateRows('users', { id: 1 }, { name: 'Jane' })
// Delete rows
await adminService.deleteRows('users', { id: 1 })
`
---
A built-in web interface for browsing and managing SQLite databases.
`typescript`
const hazo = createHazoConnect({
type: 'sqlite',
enable_admin_ui: true,
sqlite: { database_path: './database.sqlite' }
})
Or via environment variable:
`bash`
HAZO_CONNECT_ENABLE_ADMIN_UI=true
- UI: /hazo_connect/sqlite_admin/hazo_connect/api/sqlite/*
- API:
If you're using Tailwind v4, the Admin UI component requires an additional setup step to ensure Tailwind compiles the component's utility classes.
Add the following to your globals.css or main CSS file after the @import "tailwindcss" statement:
`css
@import "tailwindcss";
/ Required: Enable Tailwind to scan hazo_connect's classes /
@source "../node_modules/hazo_connect/dist";
`
Why is this needed? Tailwind v4 uses JIT compilation and only generates CSS for classes it finds in scanned files. By default, it only scans your project files, not node_modules/. The @source directive tells Tailwind to also scan the hazo_connect package for utility classes.
Without this directive: The Admin UI will have missing styles (transparent backgrounds, incorrect spacing, missing hover states, etc.).
Note: This is only required if you're using the SQLite Admin UI component. The core database functionality does not require this setup.
| Endpoint | Method | Description |
|----------|--------|-------------|
| /hazo_connect/api/sqlite/tables | GET | List all tables |/hazo_connect/api/sqlite/schema?table=users
| | GET | Get table schema |/hazo_connect/api/sqlite/data?table=users&limit=20
| | GET | Get table data |/hazo_connect/api/sqlite/data
| | POST | Insert row |/hazo_connect/api/sqlite/data
| | PATCH | Update rows |/hazo_connect/api/sqlite/data
| | DELETE | Delete rows |
---
| Operator | Description | Example |
|----------|-------------|---------|
| eq | Equals | .where('status', 'eq', 'active') |neq
| | Not equals | .where('status', 'neq', 'deleted') |gt
| | Greater than | .where('age', 'gt', 18) |gte
| | Greater than or equal | .where('age', 'gte', 18) |lt
| | Less than | .where('age', 'lt', 65) |lte
| | Less than or equal | .where('age', 'lte', 65) |like
| | Like (case-sensitive) | .where('name', 'like', '%John%') |ilike
| | Like (case-insensitive) | .where('name', 'ilike', '%john%') |in
| | In array | .whereIn('status', ['active', 'pending']) |is
| | Is null/not null | .where('deleted_at', 'is', null) |or
| | Or condition | .whereOr([...conditions]) |
---
hazo_connect is designed for server-side use only. The library uses:
- Next.js 'use server' directives
- Runtime guards that check for browser environment
- Separate entry points to prevent accidental client-side usage
If you try to use server-side code in a client component, you'll get a clear error message:
```
Error: hazo_connect/server can only be used on the server.
For client-side usage, import types from "hazo_connect" or "hazo_connect/ui".
---
- Next.js Setup Guide - Complete Next.js configuration and setup
- Troubleshooting Guide - Common issues and solutions
- Code Examples - Working examples for common patterns
- Migration Guide - Migrating from older code
- Types Reference - Complete type definitions
---
MIT © Pubs Abayasiri