npm.io
3.0.0 • Published 6d ago

@edium/halifax

Licence
MIT
Version
3.0.0
Deps
2
Size
811 kB
Vulns
0
Weekly
1.3K

Halifax

CI npm version License: MIT TypeScript

Halifax is an adapter-driven TypeScript framework for building standardized REST CRUD APIs automatically from resource definitions. It generates standards-compliant REST endpoints from your data models, wires up authentication, and exposes a dynamic query-builder endpoint for advanced read/update/delete operations.

The package is split into small, replaceable layers — nothing is imported into the core engine. Your ORM, HTTP server, and auth provider are all injected at startup.

Why Halifax?

  • Zero-boilerplate CRUD — define a resource once and get standards-compliant REST endpoints (list, read, create, update, upsert, delete, bulk) with correct status codes and a consistent error shape.
  • Adapter-driven & swappable — your HTTP framework, ORM/database, and auth provider are injected, not baked in. Switch any layer without touching your resource definitions.
  • 4 HTTP frameworks, identical behavior — Express 4/5, Fastify, HyperExpress, and Ultimate Express, all verified against one shared conformance suite.
  • Three ORM adaptersPrismaAdapter covers all six SQL databases (PostgreSQL, MySQL, MariaDB, SQL Server, CockroachDB, SQLite) with full CRUD, query builder, relations, and GraphQL. DrizzleAdapter (sub-path @edium/halifax/drizzle) covers PostgreSQL, MySQL, MariaDB, CockroachDB, and SQLite for CRUD plus stored-procedure endpoints via DrizzleSqlExecutor. SequelizeAdapter (sub-path @edium/halifax/sequelize) covers PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite with full CRUD, query builder, relation includes, GraphQL, and stored-procedure endpoints via SequelizeSqlExecutor. See README_LIMITATIONS.md for the full feature × database × adapter matrix.
  • Dynamic query-builder endpoint — let the front-end compose rich filtered/sorted/paginated queries "for free" (AND/OR/nesting, IN, BETWEEN, CONTAINS, …) without hand-writing endpoints. Fully validated — bad fields/operators return structured 4xx errors, never leaked DB internals.
  • OpenAPI 3.1 generation — zero-annotation spec generated from your resource definitions at startup. Prisma and Drizzle types are introspected automatically; custom repos annotate individual fields. Swagger UI at /docs, raw spec at /openapi.json. Disable with enabled: false for zero production overhead.
  • Multi-tenancy built in — per-resource tenant scoping with fail-closed guarantees; one tenant can never read or write another's rows. Privileged roles (admin, super-admin) can optionally bypass scoping to read across all tenants, with per-resource granularity. See README_MULTITENANCY.md.
  • Pluggable read-through caching — in-memory or Redis, per-resource TTLs, never-expire mode, automatic write-invalidation, tenant-safe keys, and a Cache-Control bust header.
  • Auth & field-level security — API key, JWT/Bearer, and Passport strategies (plus CompositeAuthStrategy to accept more than one credential per route); per-action permissions (matched against roles OR permission slugs); filterable/sortable/selectable/writable field flags; and per-field readRoles/writeRoles for role-based column visibility.
  • GraphQL endpoint — opt-in GraphQL API auto-generated from the same resource definitions as REST. Every query, mutation, filter, sort, and field-level permission works identically. Includes a GraphiQL IDE, per-resource opt-out, and full tenant bypass support. See README_GRAPHQL.md.
  • Lifecycle hooks — inject custom logic before or after any CRUD operation per resource (beforeCreate, afterCreate, beforeReadMany, beforeQuery, …). Stamp audit fields, emit events, enforce ownership, or transform results without writing a custom repository. See README_HOOKS.md.
  • Custom endpoints with full Halifax contextregisterCrudApi() returns a HalifaxApi singleton. Call api.addCustomEndpoint(method, path, roles, handler, openapi?) to register any route — aggregates (GROUP BY / HAVING), complex joins, business actions, external-service calls — while inheriting auth, role enforcement, error serialization, content negotiation, and live OpenAPI documentation automatically. Make a route public (skip auth) for health checks/login/webhooks, accept file uploads or stream binary responses with per-endpoint consumes/produces, apply a role hierarchy via authorizeCustom, or gate one route with an inline authorize predicate. Or disable all auto-CRUD on a resource and roll every route yourself. See README_CUSTOM_ENDPOINTS.md.
  • Validator-agnostic request validation — attach a schema to any custom endpoint via validate: { body, query, params } and Halifax validates + coerces the request (422 with details.fieldErrors on failure) and auto-generates the OpenAPI request schema from it. Bring your own validator: official adapters for Yup, Zod, Joi, and Valibot ship as opt-in subpaths (@edium/halifax/{yup,zod,joi,valibot}), and all four emit JSON Schema out of the box, so the schemas you already have document themselves with zero rewrites. See README_VALIDATION.md.
  • Stored-procedure endpoints — register a database routine and Halifax exposes it as its own auto-documented POST /execute/<name> route (kebab-cased, overridable), with typed/named parameters, per-procedure role gating, and OpenAPI generated from the parameter declarations. Off by default; works for routines that return rows or void on PostgreSQL, MySQL/MariaDB, and SQL Server via PrismaSqlExecutor, DrizzleSqlExecutor, or SequelizeSqlExecutor. See README_EXECUTE.md.
  • Secure-by-default permissions — single-record CRUD, the query-builder, and single-row upsert are enabled by default; the bulk whole-collection writes (updateMany, deleteMany) are off unless a resource explicitly opts in, so one bad filter can't mutate or wipe a table.
  • Companion browser client@edium/halifax-client is a typed, zero-dependency client with a fluent query builder and built-in TanStack Query helpers (queries + mutation auto-invalidation). Bring your own HTTP library (fetch, axios, ky, ofetch, superagent).
  • Type-safe & battle-tested — strict TypeScript, ESM, ships full .d.ts; hundreds of unit tests plus the full integration suite run against six real databases + Redis in CI.

New in 3.0 (breaking): validator-agnostic request validation with auto-generated OpenAPI (Yup/Zod/Joi/Valibot adapters — see README_VALIDATION.md); SequelizeAdapter — full Repository implementation for Sequelize v6 covering PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite, with relation includes, GraphQL, and stored procedures via SequelizeSqlExecutor; DrizzleAdapter MySQL/MariaDB CRUD — pass { dialect: 'mysql' } for full MySQL/MariaDB support; stored-procedure endpoints via execute for all three adapters (see README_EXECUTE.md); and secure-by-default permissions — the bulk writes allowUpdateMany/allowDeleteMany now default to false and must be opted in per resource. See the CHANGELOG for the full 3.0.0 entry and migration notes.

New in 2.7 — Custom Endpoints, fully unblocked: custom endpoints can host any route an app needs. Make one public (skip auth) with roles: null/{ auth: false }; accept file uploads or stream binary responses with per-endpoint consumes/produces; apply a role hierarchy by implementing AuthStrategy.authorizeCustom; gate a single route with an inline authorize predicate; and accept multiple credentials per route with the new CompositeAuthStrategy. All additive and backward compatible. See README_CUSTOM_ENDPOINTS.md for full docs and examples.

2.6 — Custom Endpoints: registerCrudApi() returns a HalifaxApi singleton. Use api.addCustomEndpoint(method, path, roles, handler) to register business-logic routes, aggregates, and complex joins that inherit Halifax's full auth and error-handling pipeline — with automatic OpenAPI spec updates and duplicate-route detection.

Current Support

Layer Supported
HTTP server Express 4/5, Fastify, HyperExpress, Ultimate Express
ORM / database Prisma 6 or 7 (Postgres, MySQL, MariaDB, SQL Server, CockroachDB, SQLite); Drizzle (Postgres, MySQL, MariaDB, CockroachDB, SQLite); Sequelize v6 (Postgres, MySQL, MariaDB, SQL Server, SQLite)
Auth API key, JWT/Bearer, Passport + JWT; per-field readRoles/writeRoles; role or permission slug
Caching Pluggable read-through cache (in-memory default; bring Redis, etc.)
API docs OpenAPI 3.1 spec + Swagger UI (optional, zero overhead when disabled)
GraphQL Auto-generated schema from resource definitions; opt-in; GraphiQL IDE; requires graphql ≥ 16 peer dep

Every HTTP adapter is interchangeable and behaves identically — same routes, status codes, error-body shape, and content negotiation — so you can switch frameworks without touching your resource definitions, auth, or query logic. See README_HTTP_ADAPTERS.md for per-framework usage.

The same is true across databases: the dynamic query-builder endpoint and all CRUD compile to portable Prisma Client calls (never raw SQL), so the same client request behaves identically on every database — switch engines by changing only the Prisma provider. The integration suite runs unchanged against all six engines — Postgres, MySQL, MariaDB, SQL Server, CockroachDB, and SQLite — in CI (one matrix leg per engine) to keep that honest.

Monorepo packages

Halifax ships as two packages from the same repository:

Package Description
@edium/halifax Server — auto-CRUD engine, adapters, auth, caching, OpenAPI
@edium/halifax-client Browser/Node client — typed CRUD, query builder, TanStack Query integration

Browser Client

@edium/halifax-client is the companion frontend package. It ships a fully-typed resource client, a fluent query builder, and built-in TanStack Query option factories so list/detail queries and mutations wire up in a few lines — with automatic cache invalidation on writes. Five HTTP transports ship out of the box: fetch (default), axios, ky, ofetch, and superagent — swap with one line.

pnpm add @edium/halifax-client
pnpm add @tanstack/react-query   # React
pnpm add @tanstack/vue-query     # Vue
import { HalifaxClient, QueryBuilder, SqlComparison } from '@edium/halifax-client'

const client = new HalifaxClient({ baseUrl: '/api/v1' })
const posts = client.resource<Post, Omit<Post, 'id'>, Partial<Omit<Post, 'id'>>>('posts')

// CRUD
const { results, count } = await posts.getMany({ limit: 20 })
const post = await posts.getOne(1)
await posts.createOne({ title: 'Hello', published: false })

// Query builder — sends to POST /posts/query
const q = new QueryBuilder().where('published', SqlComparison.Equal, true).limit(10)
const { results: published } = await posts.query(q)

// React — queryKey + queryFn wired automatically
const { data } = useQuery(posts.getManyOptions({ limit: 20 }))

Full docs: README_CLIENT.md


Install

pnpm add @edium/halifax
pnpm add express @prisma/client

Quick Start

import express from 'express'
import { PrismaClient } from '@prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
import {
  PrismaAdapter,
  ApiKeyAuthStrategy,
  createExpressCrudRouter,
  type ResourceDefinition
} from '@edium/halifax'

const prisma = new PrismaClient({ adapter: new PrismaPg(process.env.DATABASE_URL!) })

// Permissive + minimal by default: only `routePrefix`, `repository`, and `fields` are required.
// Every field is filterable / sortable / selectable / writable unless you turn it off, and the
// primary key is never writable. Every CRUD action is enabled unless you disable it.
const posts: ResourceDefinition = {
  routePrefix: 'posts',
  repository: new PrismaAdapter({ delegate: prisma.post }),
  fields: [{ name: 'id' }, { name: 'title' }, { name: 'content' }, { name: 'published' }]

  // Everything below is OPTIONAL — shown here just to illustrate the exceptions you *can* set:
  // name: 'Post',                       // defaults to a title-cased routePrefix ('posts' → 'Posts')
  // permissions: { allowDeleteMany: false }, // all actions on by default; list only what to disable
  // defaultLimit: 5000, maxLimit: 5000, // these are the defaults already (0 / 0 = no pagination)
}

const app = express()
app.use(express.json())
app.use(
  '/api/v1',
  createExpressCrudRouter([posts], { authStrategy: new ApiKeyAuthStrategy(process.env.API_KEY!) })
)
app.listen(3000)

Even less boilerplate. For a Prisma-backed API, createPrismaResources(prisma, Prisma.dmmf.datamodel.models) derives a fully-configured resource for every model — no fields, no routePrefix, nothing to hand-write — and you override only the exceptions per model. The hand-built resource above is the path for custom, non-Prisma repositories. See README_REPO_ADAPTERS.md for both, and README_AUTOCRUD.md for a "verbose mode" resource with every option annotated.

Documentation

Guide Contents
README_CLIENT.md @edium/halifax-client — install, transports, query builder, React & Vue TanStack Query examples
README_AUTOCRUD.md Resource definitions, field flags, ID types, pagination, query-string filtering, error shapes
README_LIMITATIONS.md Full adapter × database × feature compatibility matrix; known limitations and workarounds
README_REPO_ADAPTERS.md Prisma 7 (and 6) setup, PrismaAdapter, DrizzleAdapter, SequelizeAdapter, custom repositories
README_HTTP_ADAPTERS.md Express, Fastify, HyperExpress & Ultimate Express adapters, and custom HTTP adapters
README_AUTH.md Auth strategies (ApiKey, JWT, Passport), requiredPermissions, per-field readRoles/writeRoles
README_MULTITENANCY.md Tenant isolation: tenant options, auto-detection, scoping guarantees, fail-closed behaviour
README_QUERYBUILDER.md Query-builder payload, comparisons, nested filters, portable execution
README_CACHE.md Read-through caching: in-memory & Redis stores, never-expire, cache-bust header
README_HOOKS.md Lifecycle hooks: beforeCreate, afterCreate, beforeReadMany, beforeQuery, and every other hook
README_CUSTOM_ENDPOINTS.md Custom endpoints: HalifaxApi, addCustomEndpoint, aggregate queries, business actions, disabling CRUD
README_VALIDATION.md Validator-agnostic request validation: ISchemaValidator, Yup/Zod/Joi/Valibot adapters, auto-OpenAPI
README_EXECUTE.md Stored-procedure endpoints: execute, per-SP routes, typed named params, PrismaSqlExecutor, DrizzleSqlExecutor, SequelizeSqlExecutor
README_GRAPHQL.md GraphQL endpoint: opt-in setup, auto-generated schema, GraphiQL IDE, auth, tenant bypass for admins
README_OPENAPI.md OpenAPI 3.1 spec generation, Swagger UI, type introspection, security schemes, programmatic use
README_TYPES.md All exported type aliases, enums (SqlComparison, SqlOperator, SqlOrder), and constants
README_INTERFACES.md All exported interfaces — resource, auth, HTTP, repository, cache, Prisma, Drizzle, query AST
README_CLASSES.md All exported classes — auth strategies, HTTP adapters, ORM adapters, cache stores, error types

Examples

Runnable, self-contained examples live in examples/ — each is a complete server you can start with pnpm tsx examples/<file>.ts:

  • One per HTTP adapterhttp-express.ts (the canonical one, with an annotated "verbose mode"), http-fastify.ts, http-hyper-express.ts, http-ultimate-express.ts. Same resources, same behaviour; only the framework wiring differs.
  • One per databasedb-postgres.ts, db-mysql.ts, db-mariadb.ts, db-mssql.ts, db-cockroachdb.ts, db-sqlite.ts. Express + Prisma against each engine; only the driver adapter and connection string change.

Running Integration Tests

Halifax has two integration test layers. Prisma tests run the full CRUD + query-builder + HTTP stack against a real database engine. Sequelize tests run the same repository contract against real SQLite in-memory (always, no Docker needed) and optionally against a live database of your choice.

Prisma integration tests

The Prisma suite runs the full stack (CRUD, query-builder, HTTP adapters, stored procedures) against a real database. docker-compose.test.yml brings up one container per engine (SQLite is embedded, so it needs none); HALIFAX_DB + DATABASE_URL select the target, and globalSetup runs prisma generate + prisma db push automatically.

# All six engines at once:
docker compose -f docker-compose.test.yml up -d --wait   # postgres, mysql, mariadb, mssql, cockroachdb, redis
pnpm test:integration:all
docker compose -f docker-compose.test.yml down -v

# One specific engine (matrix script sets DATABASE_URL for you):
docker compose -f docker-compose.test.yml up -d --wait cockroachdb redis
bash scripts/integration-matrix.sh cockroachdb       # or: postgres | mysql | mariadb | mssql | sqlite

# Classic single-Postgres flow:
# .env.test → DATABASE_URL="postgresql://postgres:postgres@localhost:5432/halifax_test"
pnpm test:integration
Sequelize integration tests

The Sequelize tests use the shared repository contract (57 scenarios: full CRUD, all 17 comparison operators, AND/OR/nesting, tenant isolation) plus a stored-procedure executor suite.

# Always runs — real SQLite in-memory, no Docker needed:
pnpm vitest run tests/integration/sequelize.integration.test.ts

# Against a live database (HALIFAX_DB + DATABASE_URL — same env vars as the Prisma matrix):
HALIFAX_DB=postgres DATABASE_URL="postgresql://..." \
  pnpm vitest run tests/integration/sequelize.integration.test.ts

HALIFAX_DB=mysql    DATABASE_URL="mysql://..."      pnpm vitest run tests/integration/sequelize.integration.test.ts
HALIFAX_DB=mariadb  DATABASE_URL="mysql://..."      pnpm vitest run tests/integration/sequelize.integration.test.ts
HALIFAX_DB=mssql    DATABASE_URL="sqlserver://..."  pnpm vitest run tests/integration/sequelize.integration.test.ts
HALIFAX_DB=sqlite   DATABASE_URL="file:./test.db"   pnpm vitest run tests/integration/sequelize.integration.test.ts

# Stored-procedure executor (postgres, mysql, mssql — requires a live database):
HALIFAX_DB=postgres DATABASE_URL="postgresql://..." \
  pnpm vitest run tests/integration/sequelizeExecute.integration.test.ts

Keywords