@edium/halifax 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -1
- package/README.md +102 -17
- package/README_AUTH.md +38 -0
- package/README_AUTOCRUD.md +5 -5
- package/README_CLASSES.md +322 -0
- package/README_HOOKS.md +275 -0
- package/README_INTERFACES.md +601 -0
- package/README_OPENAPI.md +471 -0
- package/README_REPO_ADAPTERS.md +77 -0
- package/README_TYPES.md +114 -0
- package/dist/adapters/orm/drizzle/DrizzleAdapter.d.ts +128 -0
- package/dist/adapters/orm/drizzle/DrizzleAdapter.js +255 -0
- package/dist/adapters/orm/drizzle/astToDrizzle.d.ts +21 -0
- package/dist/adapters/orm/drizzle/astToDrizzle.js +121 -0
- package/dist/adapters/orm/drizzle/index.d.ts +4 -0
- package/dist/adapters/orm/drizzle/index.js +2 -0
- package/dist/adapters/orm/prisma/PrismaAdapter.d.ts +1 -1
- package/dist/adapters/orm/prisma/PrismaAdapter.js +24 -1
- package/dist/adapters/orm/prisma/astToPrisma.d.ts +1 -2
- package/dist/adapters/orm/prisma/astToPrisma.js +1 -3
- package/dist/adapters/orm/prisma/helpers.js +1 -1
- package/dist/adapters/orm/prisma/types.d.ts +11 -11
- package/dist/auth/AuthStrategy.d.ts +6 -189
- package/dist/auth/AuthStrategy.js +4 -220
- package/dist/auth/strategies/AllowAllAuthStrategy.d.ts +6 -0
- package/dist/auth/strategies/AllowAllAuthStrategy.js +6 -0
- package/dist/auth/strategies/ApiKeyAuthStrategy.d.ts +25 -0
- package/dist/auth/strategies/ApiKeyAuthStrategy.js +39 -0
- package/dist/auth/strategies/JwtClaimsAuthStrategy.d.ts +32 -0
- package/dist/auth/strategies/JwtClaimsAuthStrategy.js +52 -0
- package/dist/auth/strategies/PassportStrategies.d.ts +94 -0
- package/dist/auth/strategies/PassportStrategies.js +142 -0
- package/dist/auth/strategies/types.d.ts +70 -0
- package/dist/core/crudRouter.d.ts +11 -18
- package/dist/core/crudRouter.js +95 -390
- package/dist/core/fields.d.ts +8 -0
- package/dist/core/fields.js +14 -0
- package/dist/core/handlerUtils.d.ts +70 -0
- package/dist/core/handlerUtils.js +193 -0
- package/dist/core/handlers/create.d.ts +3 -0
- package/dist/core/handlers/create.js +26 -0
- package/dist/core/handlers/deleteMany.d.ts +3 -0
- package/dist/core/handlers/deleteMany.js +24 -0
- package/dist/core/handlers/deleteOne.d.ts +3 -0
- package/dist/core/handlers/deleteOne.js +19 -0
- package/dist/core/handlers/query.d.ts +3 -0
- package/dist/core/handlers/query.js +23 -0
- package/dist/core/handlers/readMany.d.ts +3 -0
- package/dist/core/handlers/readMany.js +18 -0
- package/dist/core/handlers/readOne.d.ts +3 -0
- package/dist/core/handlers/readOne.js +23 -0
- package/dist/core/handlers/updateMany.d.ts +3 -0
- package/dist/core/handlers/updateMany.js +34 -0
- package/dist/core/handlers/updateOne.d.ts +3 -0
- package/dist/core/handlers/updateOne.js +20 -0
- package/dist/core/handlers/upsertOne.d.ts +3 -0
- package/dist/core/handlers/upsertOne.js +20 -0
- package/dist/core/hooks.d.ts +217 -0
- package/dist/core/queryString.js +1 -1
- package/dist/core/types.d.ts +38 -29
- package/dist/core/validation.d.ts +1 -2
- package/dist/core/validation.js +1 -3
- package/dist/index.d.ts +3 -6
- package/dist/index.js +3 -6
- package/dist/openapi/generateDocsHtml.d.ts +1 -0
- package/dist/openapi/generateDocsHtml.js +47 -0
- package/dist/openapi/index.d.ts +3 -0
- package/dist/openapi/index.js +2 -0
- package/dist/openapi/specGenerator.d.ts +149 -0
- package/dist/openapi/specGenerator.js +770 -0
- package/package.json +38 -22
- package/dist/enums/SqlComparison.d.ts +0 -28
- package/dist/enums/SqlComparison.js +0 -29
- package/dist/enums/SqlOperator.d.ts +0 -5
- package/dist/enums/SqlOperator.js +0 -6
- package/dist/enums/SqlOrder.d.ts +0 -5
- package/dist/enums/SqlOrder.js +0 -6
- package/dist/interfaces/IQueryFilter.d.ts +0 -17
- package/dist/interfaces/IQueryOptions.d.ts +0 -20
- package/dist/interfaces/ISort.d.ts +0 -8
- package/dist/interfaces/ISort.js +0 -1
- /package/dist/{interfaces/IQueryFilter.js → auth/strategies/types.js} +0 -0
- /package/dist/{interfaces/IQueryOptions.js → core/hooks.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,68 @@
|
|
|
3
3
|
All notable changes to this project are documented here. This project adheres to
|
|
4
4
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## [2.2.0]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **Lifecycle hooks** — inject custom logic before or after any CRUD operation by setting
|
|
11
|
+
`hooks` on `ResourceDefinition`. All 18 hooks cover every operation the auto-CRUD engine
|
|
12
|
+
exposes:
|
|
13
|
+
|
|
14
|
+
| Category | Hooks |
|
|
15
|
+
| --------------- | ------------------------------------- |
|
|
16
|
+
| Create | `beforeCreate`, `afterCreate` |
|
|
17
|
+
| Read (list) | `beforeReadMany`, `afterReadMany` |
|
|
18
|
+
| Read (single) | `beforeReadOne`, `afterReadOne` |
|
|
19
|
+
| Update (single) | `beforeUpdateOne`, `afterUpdateOne` |
|
|
20
|
+
| Update (bulk) | `beforeUpdateMany`, `afterUpdateMany` |
|
|
21
|
+
| Upsert | `beforeUpsertOne`, `afterUpsertOne` |
|
|
22
|
+
| Delete (single) | `beforeDeleteOne`, `afterDeleteOne` |
|
|
23
|
+
| Delete (bulk) | `beforeDeleteMany`, `afterDeleteMany` |
|
|
24
|
+
| Query builder | `beforeQuery`, `afterQuery` |
|
|
25
|
+
|
|
26
|
+
**Before hooks** can return a modified data object (replacing the incoming payload) or
|
|
27
|
+
`void` to leave it unchanged. Throwing any `Error` aborts the operation and sends the
|
|
28
|
+
correct HTTP error response — use Halifax error classes (`AuthorizationError`,
|
|
29
|
+
`BadRequestError`, `UnprocessableEntityError`, …) for precise status codes.
|
|
30
|
+
|
|
31
|
+
**After hooks** can return a modified result (replacing what is sent to the client) or
|
|
32
|
+
`void`. They run after the database write but before response field-filtering
|
|
33
|
+
(`readRoles` / `selectable`), so they see every field the DB returned.
|
|
34
|
+
|
|
35
|
+
Every hook receives a `HookContext` as its last argument: `{ auth, resource, req }`.
|
|
36
|
+
|
|
37
|
+
Common patterns: stamping `createdBy` / `updatedBy` from auth context, emitting domain
|
|
38
|
+
events, enforcing ownership checks beyond what `AuthStrategy` provides, restricting
|
|
39
|
+
query-builder results to the caller's own data, and soft-delete read interception.
|
|
40
|
+
|
|
41
|
+
See [README_HOOKS.md](./README_HOOKS.md) for the full reference and examples.
|
|
42
|
+
|
|
43
|
+
- **OpenAPI 3.1 spec generation** — pass an `openapi` object to `createExpressCrudRouter` / `registerCrudApi` and Halifax generates a complete spec from your registered resources at startup. No manual annotation needed. Routes for `GET /openapi.json` and `GET /docs` (Swagger UI) are registered automatically at your mount point.
|
|
44
|
+
- Field types are introspected from the Prisma DMMF (`PrismaAdapter`) and from Drizzle column metadata (`DrizzleAdapter`) with no extra configuration. Custom / non-ORM repositories annotate individual fields with optional `type` and `format` on `FieldDefinition`.
|
|
45
|
+
- The spec documents exactly the operations your `permissions` allow — disabled actions are omitted entirely.
|
|
46
|
+
- Query-string parameters, request/response schemas, error shapes, and envelope wrapping are all reflected accurately.
|
|
47
|
+
- Auth is wired automatically: `ApiKeyAuthStrategy`, `JwtClaimsAuthStrategy`, `PassportJwtStrategy`, `Auth0JwtStrategy`, and `FirebaseJwtStrategy` each contribute the correct security scheme; `AllowAllAuthStrategy` produces no security requirement. Override with `openApiScheme()` on a custom strategy, or pass `securityScheme` directly in `openapi` options.
|
|
48
|
+
- Gate docs behind an environment check with `enabled: process.env.NODE_ENV !== 'production'` — when `enabled` is `false` (or the `openapi` key is omitted), no routes are registered and the generator is never called.
|
|
49
|
+
- Custom spec and docs paths via `specPath` and `docsPath`.
|
|
50
|
+
- `generateOpenApiSpec(resources, options)` is exported standalone for CI validation, static hosting, or piping into code generators.
|
|
51
|
+
- See [README_OPENAPI.md](./README_OPENAPI.md) for full documentation.
|
|
52
|
+
|
|
53
|
+
- **`@edium/halifax-client` companion package** — a typed browser/Node client that lives alongside this package in the same repository. Zero runtime dependencies. Bring your own HTTP client (native `fetch`, axios, ky, ofetch, or superagent — each ships a ready-made transport adapter). Features: typed CRUD methods, a fluent `QueryBuilder` that compiles to the server's query AST, and full TanStack Query integration (read query options + mutation options with auto-invalidation) built directly into `ResourceClient`. See the [client README](../halifax-client/README.md) for details.
|
|
54
|
+
|
|
55
|
+
- **Drizzle ORM adapter** — `DrizzleAdapter<TRecord, TCreate, TUpdate>` implements the full `Repository` interface against any Drizzle-compatible database (PostgreSQL, MySQL, SQLite, LibSQL). Import from the `@edium/halifax/drizzle` sub-path export; `drizzle-orm` is an optional peer dependency and is never required when unused.
|
|
56
|
+
- Field schema and OpenAPI types are derived automatically via `getTableColumns()` — no `fields` array needed.
|
|
57
|
+
- All Halifax query-AST operators are compiled to native Drizzle SQL expressions (never raw strings).
|
|
58
|
+
- Multi-tenant isolation via `withScope()` and full `executeQuery()` (query-builder endpoint) support.
|
|
59
|
+
- Primary key is auto-detected from the table schema; override with `config.idField` when using composite keys or non-standard names.
|
|
60
|
+
- See [README_REPO_ADAPTERS.md](./README_REPO_ADAPTERS.md) for usage.
|
|
61
|
+
|
|
62
|
+
- **Per-field role-based access control** — `FieldDefinition` gains two optional arrays:
|
|
63
|
+
- `readRoles: string[]` — callers whose `auth.roles` or `auth.permissions` contain none of these strings have the field stripped from every response (getOne, getMany, query, and the results of create/update/upsert). Applied at the response boundary — no extra DB round-trips.
|
|
64
|
+
- `writeRoles: string[]` — callers lacking a matching role have the field silently dropped from write bodies (same effect as `writable: false` for that caller). Callers with a matching role can write the field normally.
|
|
65
|
+
- Roles are matched against both `auth.roles` and `auth.permissions` for consistency with `requiredPermissions`.
|
|
66
|
+
- See [README_AUTH.md](./README_AUTH.md) for usage.
|
|
67
|
+
|
|
6
68
|
## [2.1.0]
|
|
7
69
|
|
|
8
70
|
### Added
|
|
@@ -124,5 +186,6 @@ First public release.
|
|
|
124
186
|
- **Auth & field-level security** — API key, JWT/Bearer, and Passport strategies; per-action
|
|
125
187
|
required permissions; and `filterable`/`sortable`/`selectable`/`writable` field flags.
|
|
126
188
|
|
|
189
|
+
[2.2.0]: https://github.com/splayfee/halifax/releases/tag/v2.2.0
|
|
190
|
+
[2.1.0]: https://github.com/splayfee/halifax/releases/tag/v2.1.0
|
|
127
191
|
[2.0.0]: https://github.com/splayfee/halifax/releases/tag/v2.0.0
|
|
128
|
-
[1.0.0]: https://github.com/splayfee/halifax/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -14,21 +14,25 @@ The package is split into small, replaceable layers — nothing is imported into
|
|
|
14
14
|
- 🚀 **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.
|
|
15
15
|
- 🧩 **Adapter-driven & swappable** — your HTTP framework, ORM/database, and auth provider are injected, not baked in. Switch any layer without touching your resource definitions.
|
|
16
16
|
- 🌐 **4 HTTP frameworks, identical behavior** — Express 4/5, Fastify, HyperExpress, and Ultimate Express, all verified against one shared conformance suite.
|
|
17
|
-
- 🗄️ **
|
|
17
|
+
- 🗄️ **Two ORM adapters, six databases** — `PrismaAdapter` covers PostgreSQL, MySQL, MariaDB, SQL Server, CockroachDB, and SQLite; `DrizzleAdapter` (sub-path `@edium/halifax/drizzle`) covers PostgreSQL, MySQL, SQLite, and LibSQL. Both compile to ORM calls (never raw SQL) so the same query behaves identically across engines.
|
|
18
18
|
- 🔎 **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.
|
|
19
|
+
- 📄 **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.
|
|
19
20
|
- 🏢 **Multi-tenancy built in** — per-resource tenant scoping with fail-closed guarantees; one tenant can never read or write another's rows.
|
|
20
21
|
- ⚡ **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.
|
|
21
|
-
- 🔐 **Auth & field-level security** — API key, JWT/Bearer, and Passport strategies; per-action permissions;
|
|
22
|
+
- 🔐 **Auth & field-level security** — API key, JWT/Bearer, and Passport strategies; per-action permissions; `filterable`/`sortable`/`selectable`/`writable` field flags; and per-field `readRoles`/`writeRoles` for role-based column visibility.
|
|
23
|
+
- 🪝 **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](./README_HOOKS.md).
|
|
24
|
+
- 📦 **Companion browser client** — [`@edium/halifax-client`](https://www.npmjs.com/package/@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).
|
|
22
25
|
- 🧪 **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.
|
|
23
26
|
|
|
24
27
|
## Current Support
|
|
25
28
|
|
|
26
|
-
| Layer | Supported
|
|
27
|
-
| -------------- |
|
|
28
|
-
| HTTP server | Express 4/5, Fastify, HyperExpress, Ultimate Express
|
|
29
|
-
| ORM / database | Prisma 6 or 7
|
|
30
|
-
| Auth | API key, JWT/Bearer, Passport + JWT
|
|
31
|
-
| Caching | Pluggable read-through cache (in-memory default; bring Redis, etc.)
|
|
29
|
+
| Layer | Supported |
|
|
30
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
31
|
+
| HTTP server | Express 4/5, Fastify, HyperExpress, Ultimate Express |
|
|
32
|
+
| ORM / database | Prisma 6 or 7 (Postgres, MySQL, MariaDB, SQL Server, CockroachDB, SQLite); Drizzle (Postgres, MySQL, SQLite, LibSQL) |
|
|
33
|
+
| Auth | API key, JWT/Bearer, Passport + JWT; per-field `readRoles`/`writeRoles` |
|
|
34
|
+
| Caching | Pluggable read-through cache (in-memory default; bring Redis, etc.) |
|
|
35
|
+
| API docs | OpenAPI 3.1 spec + Swagger UI (optional, zero overhead when disabled) |
|
|
32
36
|
|
|
33
37
|
Every HTTP adapter is interchangeable and behaves identically — same routes, status codes,
|
|
34
38
|
error-body shape, and content negotiation — so you can switch frameworks without touching
|
|
@@ -52,6 +56,81 @@ CockroachDB, and SQLite — in CI (one matrix leg per engine) to keep that hones
|
|
|
52
56
|
>
|
|
53
57
|
> **Roadmap** — community-written adapters for Drizzle, Sequelize, etc. are also welcome.
|
|
54
58
|
|
|
59
|
+
## Monorepo packages
|
|
60
|
+
|
|
61
|
+
Halifax ships as two packages from the same repository:
|
|
62
|
+
|
|
63
|
+
| Package | Description |
|
|
64
|
+
| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
|
|
65
|
+
| [`@edium/halifax`](https://www.npmjs.com/package/@edium/halifax) | Server — auto-CRUD engine, adapters, auth, caching, OpenAPI |
|
|
66
|
+
| [`@edium/halifax-client`](https://www.npmjs.com/package/@edium/halifax-client) | Browser/Node client — typed CRUD, query builder, TanStack Query integration |
|
|
67
|
+
|
|
68
|
+
## Browser Client — React & Vue
|
|
69
|
+
|
|
70
|
+
[`@edium/halifax-client`](https://www.npmjs.com/package/@edium/halifax-client) is the companion
|
|
71
|
+
frontend package. It ships a fully-typed resource client, a fluent query builder, and **built-in
|
|
72
|
+
TanStack Query option factories** so list/detail queries and mutations wire up in a few lines —
|
|
73
|
+
with automatic cache invalidation on writes.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pnpm add @edium/halifax-client
|
|
77
|
+
|
|
78
|
+
# add whichever TanStack Query adapter your framework uses
|
|
79
|
+
pnpm add @tanstack/react-query # React
|
|
80
|
+
pnpm add @tanstack/vue-query # Vue
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**React**
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
87
|
+
import { HalifaxClient } from '@edium/halifax-client'
|
|
88
|
+
|
|
89
|
+
const client = new HalifaxClient({ baseUrl: '/api/v1' })
|
|
90
|
+
const posts = client.resource<Post, NewPost, PatchPost>('posts')
|
|
91
|
+
|
|
92
|
+
// Paginated list — queryKey is managed for you
|
|
93
|
+
function usePostList(page: number) {
|
|
94
|
+
return useQuery(posts.getManyOptions({ limit: 20, offset: page * 20 }))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create with automatic list invalidation
|
|
98
|
+
function useCreatePost() {
|
|
99
|
+
const qc = useQueryClient()
|
|
100
|
+
return useMutation({
|
|
101
|
+
...posts.createOneMutation(),
|
|
102
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: posts.queryKey() })
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Vue**
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { computed, ref } from 'vue'
|
|
111
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
|
|
112
|
+
import { HalifaxClient } from '@edium/halifax-client'
|
|
113
|
+
|
|
114
|
+
const client = new HalifaxClient({ baseUrl: '/api/v1' })
|
|
115
|
+
const posts = client.resource<Post, NewPost, PatchPost>('posts')
|
|
116
|
+
|
|
117
|
+
// Reactive query — re-fetches automatically when page changes
|
|
118
|
+
const page = ref(0)
|
|
119
|
+
const postList = useQuery(computed(() => posts.getManyOptions({ limit: 20, offset: page.value * 20 })))
|
|
120
|
+
|
|
121
|
+
// Mutation with cache invalidation
|
|
122
|
+
const qc = useQueryClient()
|
|
123
|
+
const createPost = useMutation({
|
|
124
|
+
...posts.createOneMutation(),
|
|
125
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: posts.queryKey() })
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Five HTTP transports are included out of the box: `fetch` (default), `axios`, `ky`, `ofetch`, and
|
|
130
|
+
`superagent` — swap with one line. Full docs: [README_CLIENT.md](./README_CLIENT.md).
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
55
134
|
## Install
|
|
56
135
|
|
|
57
136
|
```bash
|
|
@@ -107,15 +186,21 @@ app.listen(3000)
|
|
|
107
186
|
|
|
108
187
|
## Documentation
|
|
109
188
|
|
|
110
|
-
| Guide | Contents
|
|
111
|
-
| ---------------------------------------------------- |
|
|
112
|
-
| [
|
|
113
|
-
| [
|
|
114
|
-
| [
|
|
115
|
-
| [
|
|
116
|
-
| [
|
|
117
|
-
| [
|
|
118
|
-
| [
|
|
189
|
+
| Guide | Contents |
|
|
190
|
+
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
191
|
+
| [README_CLIENT.md](./README_CLIENT.md) | `@edium/halifax-client` — install, transports, query builder, React & Vue TanStack Query examples |
|
|
192
|
+
| [README_AUTOCRUD.md](./README_AUTOCRUD.md) | Resource definitions, field flags, ID types, pagination, query-string filtering, error shapes |
|
|
193
|
+
| [README_REPO_ADAPTERS.md](./README_REPO_ADAPTERS.md) | Prisma 7 (and 6) setup, `PrismaAdapter`, `DrizzleAdapter`, capabilities, custom repositories |
|
|
194
|
+
| [README_HTTP_ADAPTERS.md](./README_HTTP_ADAPTERS.md) | Express, Fastify, HyperExpress & Ultimate Express adapters, and custom HTTP adapters |
|
|
195
|
+
| [README_AUTH.md](./README_AUTH.md) | Auth strategies (`ApiKey`, `JWT`, `Passport`), `requiredPermissions`, per-field `readRoles`/`writeRoles` |
|
|
196
|
+
| [README_MULTITENANCY.md](./README_MULTITENANCY.md) | Tenant isolation: `tenant` options, auto-detection, scoping guarantees, fail-closed behaviour |
|
|
197
|
+
| [README_QUERYBUILDER.md](./README_QUERYBUILDER.md) | Query-builder payload, comparisons, nested filters, portable execution |
|
|
198
|
+
| [README_CACHE.md](./README_CACHE.md) | Read-through caching: in-memory & Redis stores, never-expire, cache-bust header |
|
|
199
|
+
| [README_HOOKS.md](./README_HOOKS.md) | Lifecycle hooks: `beforeCreate`, `afterCreate`, `beforeReadMany`, `beforeQuery`, and every other hook |
|
|
200
|
+
| [README_OPENAPI.md](./README_OPENAPI.md) | OpenAPI 3.1 spec generation, Swagger UI, type introspection, security schemes, programmatic use |
|
|
201
|
+
| [README_TYPES.md](./README_TYPES.md) | All exported type aliases, enums (`SqlComparison`, `SqlOperator`, `SqlOrder`), and constants |
|
|
202
|
+
| [README_INTERFACES.md](./README_INTERFACES.md) | All exported interfaces — resource, auth, HTTP, repository, cache, Prisma, Drizzle, query AST |
|
|
203
|
+
| [README_CLASSES.md](./README_CLASSES.md) | All exported classes — auth strategies, HTTP adapters, ORM adapters, cache stores, error types |
|
|
119
204
|
|
|
120
205
|
## Examples
|
|
121
206
|
|
package/README_AUTH.md
CHANGED
|
@@ -125,6 +125,44 @@ export const authStrategy = new PassportJwtStrategy({
|
|
|
125
125
|
})
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
## Per-Field Role-Based Access Control
|
|
129
|
+
|
|
130
|
+
`FieldDefinition` supports two optional arrays for column-level visibility:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const postResource: ResourceDefinition = {
|
|
134
|
+
routePrefix: 'posts',
|
|
135
|
+
repository: new PrismaAdapter({ delegate: prisma.post }),
|
|
136
|
+
fields: [
|
|
137
|
+
{ name: 'id' },
|
|
138
|
+
{ name: 'title' },
|
|
139
|
+
{ name: 'content' },
|
|
140
|
+
// Only users with role 'editor' or 'admin' can read or write the `status` field.
|
|
141
|
+
{ name: 'status', readRoles: ['editor', 'admin'], writeRoles: ['editor', 'admin'] },
|
|
142
|
+
// Any authenticated user can read `authorId`, but only admins can set it.
|
|
143
|
+
{ name: 'authorId', writeRoles: ['admin'] },
|
|
144
|
+
// `internalNote` is invisible to everyone except admins.
|
|
145
|
+
{ name: 'internalNote', readRoles: ['admin'], writeRoles: ['admin'] }
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### `readRoles`
|
|
151
|
+
|
|
152
|
+
When a field has `readRoles`, it is **stripped from every response** for any caller whose `auth.roles` and `auth.permissions` contain none of the listed values. This applies uniformly across `getOne`, `getMany`, `query`, and the records returned by `createOne`, `updateOne`, and `upsertOne` — no extra configuration per operation. The field is never sent to the database `SELECT`; it is removed at the response boundary after the auth context is resolved.
|
|
153
|
+
|
|
154
|
+
### `writeRoles`
|
|
155
|
+
|
|
156
|
+
When a field has `writeRoles`, callers without a matching role have the field **silently dropped** from write bodies before the repository sees them. The effect is identical to `writable: false` for that caller. Callers with a matching role can write the field normally.
|
|
157
|
+
|
|
158
|
+
### Role matching
|
|
159
|
+
|
|
160
|
+
Both `readRoles` and `writeRoles` are matched against both `auth.roles` and `auth.permissions` — a caller needs at least one match in either array. This is consistent with how `requiredPermissions` works.
|
|
161
|
+
|
|
162
|
+
### Primary key note
|
|
163
|
+
|
|
164
|
+
The primary key is protected by `writable: false` by default regardless of `writeRoles`. Setting `writeRoles` on the primary key has no additional effect.
|
|
165
|
+
|
|
128
166
|
## Per-Action Permission Requirements
|
|
129
167
|
|
|
130
168
|
`requiredPermissions` on a resource maps each CRUD action to a list of roles or permission strings. The authenticated user must possess at least one entry from the list (matched against both `auth.roles` and `auth.permissions`).
|
package/README_AUTOCRUD.md
CHANGED
|
@@ -158,11 +158,11 @@ createExpressCrudRouter(resources, { authStrategy, envelope: 'data' })
|
|
|
158
158
|
|
|
159
159
|
The wrap is **uniform** — it nests the entire body, it does not reshape it:
|
|
160
160
|
|
|
161
|
-
| Endpoint
|
|
162
|
-
|
|
|
163
|
-
| List / query
|
|
164
|
-
| Read / create
|
|
165
|
-
| Delete one
|
|
161
|
+
| Endpoint | Bare (default) | `envelope: 'data'` |
|
|
162
|
+
| ------------- | -------------------- | ------------------------------ |
|
|
163
|
+
| List / query | `{ count, results }` | `{ data: { count, results } }` |
|
|
164
|
+
| Read / create | `{ id, ... }` | `{ data: { id, ... } }` |
|
|
165
|
+
| Delete one | `{ deleted: true }` | `{ data: { deleted: true } }` |
|
|
166
166
|
|
|
167
167
|
Notes:
|
|
168
168
|
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Classes — `@edium/halifax`
|
|
2
|
+
|
|
3
|
+
All publicly exported TypeScript classes. Type aliases are in [README_TYPES.md](./README_TYPES.md); interfaces are in [README_INTERFACES.md](./README_INTERFACES.md).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Auth strategies
|
|
8
|
+
|
|
9
|
+
All auth strategy classes implement `AuthStrategy`. Pass an instance to `CrudApiOptions.authStrategy`.
|
|
10
|
+
|
|
11
|
+
### `AllowAllAuthStrategy`
|
|
12
|
+
|
|
13
|
+
Import: `@edium/halifax`
|
|
14
|
+
|
|
15
|
+
Passes every request through without any authentication check. Returns `{ isAuthenticated: true }` for every caller. Suitable for public APIs or local development only.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { AllowAllAuthStrategy } from '@edium/halifax'
|
|
19
|
+
createExpressCrudRouter(resources, { authStrategy: new AllowAllAuthStrategy() })
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
### `ApiKeyAuthStrategy`
|
|
25
|
+
|
|
26
|
+
Import: `@edium/halifax`
|
|
27
|
+
|
|
28
|
+
Reads a request header and compares it against a static shared secret.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
new ApiKeyAuthStrategy(expectedApiKey: string, headerName?: string)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Parameter | Default | Description |
|
|
35
|
+
| ---------------- | ------------- | ----------------------------------- |
|
|
36
|
+
| `expectedApiKey` | required | The secret key callers must supply. |
|
|
37
|
+
| `headerName` | `'x-api-key'` | Header to read the key from. |
|
|
38
|
+
|
|
39
|
+
- Missing header → 401 Unauthorized
|
|
40
|
+
- Wrong key → 403 Forbidden
|
|
41
|
+
- OpenAPI: documents `apiKey` in header (uses the configured `headerName`).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
### `JwtClaimsAuthStrategy`
|
|
46
|
+
|
|
47
|
+
Import: `@edium/halifax`
|
|
48
|
+
|
|
49
|
+
Extracts a Bearer token from the `Authorization` header and passes it to your verify callback. No Passport dependency — use this for any JWT library (`jsonwebtoken`, `jose`, etc.).
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
new JwtClaimsAuthStrategy(
|
|
53
|
+
verifyToken: (token: string, req: HttpRequest) => AuthContext | Promise<AuthContext>
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The verify callback receives the raw token string and must return an `AuthContext`. Throw any error to reject (→ 401). The built-in `authorize` checks `requiredPermissions` against `auth.roles` and `auth.permissions`.
|
|
58
|
+
|
|
59
|
+
OpenAPI: documents `http` bearer JWT.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### `PassportJwtStrategy`
|
|
64
|
+
|
|
65
|
+
Import: `@edium/halifax`
|
|
66
|
+
|
|
67
|
+
Delegates JWT verification to a Passport strategy already registered on your Passport instance. Use when you have an existing `passport-jwt` setup.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
new PassportJwtStrategy(options: PassportJwtStrategyOptions)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
See `PassportJwtStrategyOptions` in [README_INTERFACES.md](./README_INTERFACES.md). The built-in `authorize` checks `requiredPermissions` against `auth.roles` and `auth.permissions`.
|
|
74
|
+
|
|
75
|
+
OpenAPI: documents `http` bearer JWT.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### `PassportSessionStrategy`
|
|
80
|
+
|
|
81
|
+
Import: `@edium/halifax`
|
|
82
|
+
|
|
83
|
+
Authenticates using Passport session cookies. Reads `req.raw.user` (populated by Passport's session middleware running before Halifax).
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
new PassportSessionStrategy(mapUser?: (user: unknown) => AuthContext)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Parameter | Description |
|
|
90
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
91
|
+
| `mapUser` | Optional function to map the raw session user object to an `AuthContext`. Default reads `sub`/`id`, `roles`, and `permissions`. |
|
|
92
|
+
|
|
93
|
+
**Prerequisites** — add before mounting Halifax:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
app.use(session({ secret: '...' }))
|
|
97
|
+
app.use(passport.initialize())
|
|
98
|
+
app.use(passport.session())
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Missing `req.user` (session expired or not logged in) → 401.
|
|
102
|
+
|
|
103
|
+
OpenAPI: documents `apiKey` in cookie (`connect.sid`).
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### `PassportAuthStrategy`
|
|
108
|
+
|
|
109
|
+
Import: `@edium/halifax`
|
|
110
|
+
|
|
111
|
+
Generic Passport wrapper. Takes a function that calls your Passport strategy and returns an `AuthContext`. Use when neither `PassportJwtStrategy` nor `PassportSessionStrategy` fits.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
new PassportAuthStrategy(
|
|
115
|
+
authenticateWithPassport: (req: HttpRequest) => AuthContext | Promise<AuthContext>
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### `Auth0JwtStrategy`
|
|
122
|
+
|
|
123
|
+
Import: `@edium/halifax`
|
|
124
|
+
|
|
125
|
+
Alias of `JwtClaimsAuthStrategy`. Identical in every way — the name signals intent when your tokens come from Auth0.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
new Auth0JwtStrategy(verifyToken)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### `FirebaseJwtStrategy`
|
|
134
|
+
|
|
135
|
+
Import: `@edium/halifax`
|
|
136
|
+
|
|
137
|
+
Alias of `JwtClaimsAuthStrategy`. Identical in every way — the name signals intent when your tokens are Firebase ID tokens.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
new FirebaseJwtStrategy(verifyToken)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## HTTP server adapters
|
|
146
|
+
|
|
147
|
+
Each adapter wraps a framework app instance and implements `HttpServer`. Pass to `registerCrudApi`, or use the corresponding `create*CrudRouter` / `create*CrudPlugin` helper instead.
|
|
148
|
+
|
|
149
|
+
### `ExpressHttpServer`
|
|
150
|
+
|
|
151
|
+
Import: `@edium/halifax`
|
|
152
|
+
|
|
153
|
+
Wraps an Express (4 or 5) `Application` or `Router`. Used internally by `createExpressCrudRouter`.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
new ExpressHttpServer(app: ExpressAppLike)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### `FastifyHttpServer`
|
|
162
|
+
|
|
163
|
+
Import: `@edium/halifax`
|
|
164
|
+
|
|
165
|
+
Wraps a Fastify instance. Used internally by `createFastifyCrudPlugin`.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
new FastifyHttpServer(app: FastifyAppLike)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### `HyperExpressHttpServer`
|
|
174
|
+
|
|
175
|
+
Import: `@edium/halifax`
|
|
176
|
+
|
|
177
|
+
Wraps a HyperExpress `Server` instance. Used internally by `createHyperExpressCrudRouter`.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
new HyperExpressHttpServer(app: HyperExpressAppLike)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### `UltimateExpressHttpServer`
|
|
186
|
+
|
|
187
|
+
Import: `@edium/halifax`
|
|
188
|
+
|
|
189
|
+
Wraps an Ultimate Express `App` instance. Used internally by `createUltimateExpressCrudRouter`.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
new UltimateExpressHttpServer(app: UltimateExpressAppLike)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Repository adapters
|
|
198
|
+
|
|
199
|
+
### `PrismaAdapter<TRecord, TCreate, TUpdate>`
|
|
200
|
+
|
|
201
|
+
Import: `@edium/halifax`
|
|
202
|
+
|
|
203
|
+
Implements the full `Repository` interface against any Prisma-supported database. Supports all nine CRUD operations, the query-builder endpoint, multi-tenant scoping (`withScope`), and relation eager-loading (`?include=`). Field schema and OpenAPI types are auto-derived from the Prisma DMMF when a `model` is provided.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
new PrismaAdapter<TRecord, TCreate, TUpdate>(options: PrismaAdapterOptions)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
See `PrismaAdapterOptions` in [README_INTERFACES.md](./README_INTERFACES.md).
|
|
210
|
+
|
|
211
|
+
**Static method:** `PrismaAdapter` has no static methods. For auto-generating resources from all Prisma models, use `createPrismaResources` (a standalone function).
|
|
212
|
+
|
|
213
|
+
**Capabilities reported:**
|
|
214
|
+
|
|
215
|
+
- `supportsIncludes: true`
|
|
216
|
+
- `supportsCreateManyReturn: <returnCreated option>`
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
### `DrizzleAdapter<TRecord, TCreate, TUpdate>`
|
|
221
|
+
|
|
222
|
+
Import: `@edium/halifax/drizzle`
|
|
223
|
+
|
|
224
|
+
Implements the full `Repository` interface against any Drizzle-compatible database (PostgreSQL, MySQL, SQLite, LibSQL). Field schema and OpenAPI types are auto-derived from the Drizzle table definition via `getTableColumns()`. Supports the query-builder endpoint (`executeQuery`), multi-tenant scoping (`withScope`), and field projection. `drizzle-orm` is a required peer dependency when this adapter is used.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { DrizzleAdapter } from '@edium/halifax/drizzle'
|
|
228
|
+
|
|
229
|
+
new DrizzleAdapter<TRecord, TCreate, TUpdate>(
|
|
230
|
+
db: AnyDrizzleDB,
|
|
231
|
+
table: Table,
|
|
232
|
+
config?: DrizzleAdapterConfig,
|
|
233
|
+
scope?: TenantScope | null
|
|
234
|
+
)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
See `DrizzleAdapterConfig` in [README_INTERFACES.md](./README_INTERFACES.md).
|
|
238
|
+
|
|
239
|
+
**Static method:**
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
DrizzleAdapter.fieldsFromTable(table: Table, idField?: string): FieldDefinition[]
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Derives a Halifax field schema from a Drizzle table without constructing a full adapter instance. Useful for inspecting or overriding the derived fields before passing them to a `ResourceDefinition`.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Cache stores
|
|
250
|
+
|
|
251
|
+
Both implement `CacheStore`. Pass to `CrudApiOptions.cache.store`.
|
|
252
|
+
|
|
253
|
+
### `InMemoryCacheStore`
|
|
254
|
+
|
|
255
|
+
Import: `@edium/halifax`
|
|
256
|
+
|
|
257
|
+
Process-local in-memory cache. The default when no `store` is configured. Simple and zero-dependency, but does not persist across restarts and is not shared across processes or instances.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
new InMemoryCacheStore()
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
### `RedisCacheStore`
|
|
266
|
+
|
|
267
|
+
Import: `@edium/halifax`
|
|
268
|
+
|
|
269
|
+
Redis-backed distributed cache store. Accepts any Redis client satisfying `RedisLikeClient` — `redis` v4 works directly.
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
new RedisCacheStore(client: RedisLikeClient)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { createClient } from 'redis'
|
|
277
|
+
import { RedisCacheStore } from '@edium/halifax'
|
|
278
|
+
|
|
279
|
+
const redis = createClient({ url: process.env.REDIS_URL })
|
|
280
|
+
await redis.connect()
|
|
281
|
+
|
|
282
|
+
createExpressCrudRouter(resources, {
|
|
283
|
+
cache: { store: new RedisCacheStore(redis), ttlSeconds: 60 }
|
|
284
|
+
})
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Error classes
|
|
290
|
+
|
|
291
|
+
All error classes extend `HttpError`. Throw these inside a custom `AuthStrategy` or `Repository` to return the corresponding HTTP status with a structured `{ errors: [...] }` body. Halifax catches `HttpError` subclasses at the route boundary and serializes them automatically.
|
|
292
|
+
|
|
293
|
+
### `HttpError` (abstract)
|
|
294
|
+
|
|
295
|
+
Import: `@edium/halifax`
|
|
296
|
+
|
|
297
|
+
Base class for all Halifax HTTP errors. Not instantiated directly.
|
|
298
|
+
|
|
299
|
+
| Property | Type | Description |
|
|
300
|
+
| --------- | --------- | ------------------------------------------------------- |
|
|
301
|
+
| `status` | `number` | HTTP status code. |
|
|
302
|
+
| `message` | `string` | Human-readable error message. |
|
|
303
|
+
| `details` | `unknown` | Optional structured details included in the error body. |
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### Error subclasses
|
|
308
|
+
|
|
309
|
+
| Class | Status | When to throw |
|
|
310
|
+
| --------------------------- | ------ | -------------------------------------------------------------------------------------------------------------- |
|
|
311
|
+
| `AuthenticationError` | 401 | Missing or invalid credentials. Throw from `AuthStrategy.authenticate`. |
|
|
312
|
+
| `AuthorizationError` | 403 | Valid credentials but insufficient permissions. Throw from `AuthStrategy.authenticate` or `authorize`. |
|
|
313
|
+
| `BadRequestError` | 400 | Malformed input — invalid ID format, bad query params, bad body structure. |
|
|
314
|
+
| `NotFoundError` | 404 | Record with the given ID does not exist. Throw from a custom `Repository.getOne`. |
|
|
315
|
+
| `MethodNotAllowedError` | 405 | HTTP method not enabled for this resource. Used internally by Halifax. |
|
|
316
|
+
| `NotAcceptableError` | 406 | Client `Accept` header excludes `application/json`. Used internally by Halifax. |
|
|
317
|
+
| `UnsupportedMediaTypeError` | 415 | Body-carrying request with non-JSON `Content-Type`. Used internally by Halifax. |
|
|
318
|
+
| `UnprocessableEntityError` | 422 | Unknown fields in body, missing required filter, empty update payload. |
|
|
319
|
+
| `NotImplementedError` | 501 | Repository does not support this operation (e.g. `upsertOne` not implemented). |
|
|
320
|
+
| `ServerError` | 500 | Unexpected internal error. Halifax uses this as a catch-all; throw it from a custom adapter for explicit 500s. |
|
|
321
|
+
|
|
322
|
+
All constructors accept `(message: string, details?: unknown)`.
|