@edium/halifax 2.0.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +78 -1
  2. package/README.md +102 -17
  3. package/README_AUTH.md +38 -0
  4. package/README_AUTOCRUD.md +33 -0
  5. package/README_CLASSES.md +322 -0
  6. package/README_HOOKS.md +275 -0
  7. package/README_INTERFACES.md +601 -0
  8. package/README_OPENAPI.md +471 -0
  9. package/README_REPO_ADAPTERS.md +77 -0
  10. package/README_TYPES.md +114 -0
  11. package/dist/adapters/orm/drizzle/DrizzleAdapter.d.ts +128 -0
  12. package/dist/adapters/orm/drizzle/DrizzleAdapter.js +255 -0
  13. package/dist/adapters/orm/drizzle/astToDrizzle.d.ts +21 -0
  14. package/dist/adapters/orm/drizzle/astToDrizzle.js +121 -0
  15. package/dist/adapters/orm/drizzle/index.d.ts +4 -0
  16. package/dist/adapters/orm/drizzle/index.js +2 -0
  17. package/dist/adapters/orm/prisma/PrismaAdapter.d.ts +1 -1
  18. package/dist/adapters/orm/prisma/PrismaAdapter.js +24 -1
  19. package/dist/adapters/orm/prisma/astToPrisma.d.ts +1 -2
  20. package/dist/adapters/orm/prisma/astToPrisma.js +1 -3
  21. package/dist/adapters/orm/prisma/helpers.js +1 -1
  22. package/dist/adapters/orm/prisma/types.d.ts +11 -11
  23. package/dist/auth/AuthStrategy.d.ts +6 -189
  24. package/dist/auth/AuthStrategy.js +4 -220
  25. package/dist/auth/strategies/AllowAllAuthStrategy.d.ts +6 -0
  26. package/dist/auth/strategies/AllowAllAuthStrategy.js +6 -0
  27. package/dist/auth/strategies/ApiKeyAuthStrategy.d.ts +25 -0
  28. package/dist/auth/strategies/ApiKeyAuthStrategy.js +39 -0
  29. package/dist/auth/strategies/JwtClaimsAuthStrategy.d.ts +32 -0
  30. package/dist/auth/strategies/JwtClaimsAuthStrategy.js +52 -0
  31. package/dist/auth/strategies/PassportStrategies.d.ts +94 -0
  32. package/dist/auth/strategies/PassportStrategies.js +142 -0
  33. package/dist/auth/strategies/types.d.ts +70 -0
  34. package/dist/core/crudRouter.d.ts +16 -16
  35. package/dist/core/crudRouter.js +98 -368
  36. package/dist/core/fields.d.ts +8 -0
  37. package/dist/core/fields.js +14 -0
  38. package/dist/core/handlerUtils.d.ts +70 -0
  39. package/dist/core/handlerUtils.js +193 -0
  40. package/dist/core/handlers/create.d.ts +3 -0
  41. package/dist/core/handlers/create.js +26 -0
  42. package/dist/core/handlers/deleteMany.d.ts +3 -0
  43. package/dist/core/handlers/deleteMany.js +24 -0
  44. package/dist/core/handlers/deleteOne.d.ts +3 -0
  45. package/dist/core/handlers/deleteOne.js +19 -0
  46. package/dist/core/handlers/query.d.ts +3 -0
  47. package/dist/core/handlers/query.js +23 -0
  48. package/dist/core/handlers/readMany.d.ts +3 -0
  49. package/dist/core/handlers/readMany.js +18 -0
  50. package/dist/core/handlers/readOne.d.ts +3 -0
  51. package/dist/core/handlers/readOne.js +23 -0
  52. package/dist/core/handlers/updateMany.d.ts +3 -0
  53. package/dist/core/handlers/updateMany.js +34 -0
  54. package/dist/core/handlers/updateOne.d.ts +3 -0
  55. package/dist/core/handlers/updateOne.js +20 -0
  56. package/dist/core/handlers/upsertOne.d.ts +3 -0
  57. package/dist/core/handlers/upsertOne.js +20 -0
  58. package/dist/core/hooks.d.ts +217 -0
  59. package/dist/core/queryString.js +1 -1
  60. package/dist/core/types.d.ts +48 -29
  61. package/dist/core/validation.d.ts +1 -2
  62. package/dist/core/validation.js +1 -3
  63. package/dist/index.d.ts +3 -6
  64. package/dist/index.js +3 -6
  65. package/dist/openapi/generateDocsHtml.d.ts +1 -0
  66. package/dist/openapi/generateDocsHtml.js +47 -0
  67. package/dist/openapi/index.d.ts +3 -0
  68. package/dist/openapi/index.js +2 -0
  69. package/dist/openapi/specGenerator.d.ts +149 -0
  70. package/dist/openapi/specGenerator.js +770 -0
  71. package/package.json +38 -22
  72. package/dist/enums/SqlComparison.d.ts +0 -28
  73. package/dist/enums/SqlComparison.js +0 -29
  74. package/dist/enums/SqlOperator.d.ts +0 -5
  75. package/dist/enums/SqlOperator.js +0 -6
  76. package/dist/enums/SqlOrder.d.ts +0 -5
  77. package/dist/enums/SqlOrder.js +0 -6
  78. package/dist/interfaces/IQueryFilter.d.ts +0 -17
  79. package/dist/interfaces/IQueryOptions.d.ts +0 -20
  80. package/dist/interfaces/ISort.d.ts +0 -8
  81. package/dist/interfaces/ISort.js +0 -1
  82. /package/dist/{interfaces/IQueryFilter.js → auth/strategies/types.js} +0 -0
  83. /package/dist/{interfaces/IQueryOptions.js → core/hooks.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,82 @@
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
+
68
+ ## [2.1.0]
69
+
70
+ ### Added
71
+
72
+ - **Configurable response envelope.** A new `envelope` option wraps every success response body
73
+ under a single key (e.g. `envelope: 'data'` → `{ "data": <body> }`). Set it API-wide on
74
+ `createExpressCrudRouter`/`registerCrudApi` options, or per resource on `ResourceDefinition`
75
+ (the per-resource setting wins, including an explicit `null`/`''` to opt a single resource out
76
+ of an API-wide envelope). The wrap is uniform across list, single, create/update/upsert, and
77
+ the delete confirmation; **error responses are never enveloped**, keeping one stable error
78
+ contract. Applied at the response boundary (after the cache), so cached payloads are
79
+ envelope-agnostic. Eases adopting Halifax behind clients that expect a legacy `{ data: ... }`
80
+ shape. Defaults to off — fully backward compatible.
81
+
6
82
  ## [2.0.0]
7
83
 
8
84
  A breaking release with two themes: **permissive, minimal-by-default resource definitions**
@@ -110,5 +186,6 @@ First public release.
110
186
  - **Auth & field-level security** — API key, JWT/Bearer, and Passport strategies; per-action
111
187
  required permissions; and `filterable`/`sortable`/`selectable`/`writable` field flags.
112
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
113
191
  [2.0.0]: https://github.com/splayfee/halifax/releases/tag/v2.0.0
114
- [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
- - 🗄️ **Six databases, one adapter** — PostgreSQL, MySQL, MariaDB, SQL Server, CockroachDB, and SQLite via [Prisma](https://www.prisma.io/). The query builder compiles to portable Prisma calls (never raw SQL), so the **same client request behaves identically on every database** — switch engines by changing one line. Every engine is verified in CI against a real database — one matrix leg each, the same suite on all.
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; and `filterable`/`sortable`/`selectable`/`writable` flags enforced on every request.
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 + Postgres, MySQL, MariaDB, SQL Server, CockroachDB, SQLite |
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
- | [README_AUTOCRUD.md](./README_AUTOCRUD.md) | Resource definitions, field flags, ID types, pagination, query-string filtering, error shapes |
113
- | [README_REPO_ADAPTERS.md](./README_REPO_ADAPTERS.md) | Prisma 7 (and 6) setup, `PrismaAdapter` options, capabilities, custom repositories |
114
- | [README_HTTP_ADAPTERS.md](./README_HTTP_ADAPTERS.md) | Express, Fastify, HyperExpress & Ultimate Express adapters, and custom HTTP adapters |
115
- | [README_AUTH.md](./README_AUTH.md) | Auth strategies (`ApiKey`, `JWT`, `Passport`), `requiredPermissions`, custom `authorize` |
116
- | [README_MULTITENANCY.md](./README_MULTITENANCY.md) | Tenant isolation: `tenant` options, auto-detection, scoping guarantees, fail-closed behaviour |
117
- | [README_QUERYBUILDER.md](./README_QUERYBUILDER.md) | Query-builder payload, comparisons, nested filters, portable Prisma execution |
118
- | [README_CACHE.md](./README_CACHE.md) | Read-through caching: in-memory & Redis stores, never-expire, cache-bust header |
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`).
@@ -141,6 +141,39 @@ always pulled all rows:
141
141
  { defaultLimit: 0, maxLimit: 0 } // no pagination — return everything
142
142
  ```
143
143
 
144
+ ## Response Envelope
145
+
146
+ By default every success response is sent **bare** — a list is `{ count, results }`, a single
147
+ record is the object itself, a delete is `{ deleted: true }`. To wrap every success body under a
148
+ single key (handy when adopting Halifax behind a client that expects a legacy `{ data: ... }`
149
+ shape), set `envelope` — API-wide or per resource:
150
+
151
+ ```ts
152
+ // API-wide: every resource's success body is wrapped under "data"
153
+ createExpressCrudRouter(resources, { authStrategy, envelope: 'data' })
154
+
155
+ // Per resource (wins over the API-wide setting):
156
+ { routePrefix: 'posts', repository, envelope: 'data' }
157
+ ```
158
+
159
+ The wrap is **uniform** — it nests the entire body, it does not reshape it:
160
+
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
+
167
+ Notes:
168
+
169
+ - **Errors are never enveloped** — they always use the `{ errors: [...] }` shape (see below), so
170
+ clients have one stable error contract regardless of the success envelope.
171
+ - **Precedence** — an explicit per-resource `envelope` always wins, including `null` or `''`,
172
+ which opts a single resource out of an API-wide envelope. Omit it to inherit the API default.
173
+ - `null`, `undefined`, and `''` all mean "no envelope" (an empty key is rejected as meaningless).
174
+ - The envelope is applied at the response boundary, after the read-through cache, so cached
175
+ payloads are envelope-agnostic.
176
+
144
177
  ## Query-String Filtering and Pagination
145
178
 
146
179
  ```
@@ -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)`.