@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.
Files changed (83) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/README.md +102 -17
  3. package/README_AUTH.md +38 -0
  4. package/README_AUTOCRUD.md +5 -5
  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 +11 -18
  35. package/dist/core/crudRouter.js +95 -390
  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 +38 -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
@@ -0,0 +1,471 @@
1
+ # OpenAPI Support
2
+
3
+ Halifax can automatically generate a complete [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) specification from your registered resources — no manual annotation required. For Prisma-backed resources, field types are introspected directly from the DMMF schema. For custom repositories, you can optionally annotate individual fields.
4
+
5
+ Two routes are added automatically at your mount point:
6
+
7
+ | Route | Description |
8
+ | ------------------- | ------------------------------------------- |
9
+ | `GET /openapi.json` | Raw OpenAPI 3.1 spec (JSON) |
10
+ | `GET /docs` | Swagger UI — interactive browser-based docs |
11
+
12
+ ---
13
+
14
+ ## Quick start
15
+
16
+ ```ts
17
+ import { createExpressCrudRouter } from '@edium/halifax'
18
+
19
+ app.use(
20
+ '/api/v1',
21
+ createExpressCrudRouter(resources, {
22
+ openapi: {
23
+ title: 'My API',
24
+ version: '1.0.0',
25
+ servers: [{ url: 'https://api.example.com/v1' }]
26
+ }
27
+ })
28
+ )
29
+ ```
30
+
31
+ Then visit:
32
+
33
+ - **http://localhost:3000/api/v1/docs** — Swagger UI
34
+ - **http://localhost:3000/api/v1/openapi.json** — raw spec
35
+
36
+ ---
37
+
38
+ ## Enabling only in non-production environments
39
+
40
+ Use the `enabled` flag to gate docs behind an environment check:
41
+
42
+ ```ts
43
+ openapi: {
44
+ enabled: process.env.NODE_ENV !== 'production',
45
+ title: 'My API',
46
+ version: '1.0.0'
47
+ }
48
+ ```
49
+
50
+ When `enabled` is `false` (or the `openapi` key is omitted entirely), no extra routes are registered and the spec generator is never called — zero overhead in production.
51
+
52
+ ### Recommended environment pattern
53
+
54
+ ```ts
55
+ const openApiConfig =
56
+ process.env.ENABLE_DOCS === 'true'
57
+ ? {
58
+ title: 'My API',
59
+ version: '1.0.0',
60
+ servers: [{ url: `${process.env.BASE_URL}/api/v1` }]
61
+ }
62
+ : undefined
63
+
64
+ app.use(
65
+ '/api/v1',
66
+ createExpressCrudRouter(resources, {
67
+ openapi: openApiConfig
68
+ })
69
+ )
70
+ ```
71
+
72
+ Set `ENABLE_DOCS=true` in `.env.development`, `.env.test`, and `.env.staging`. Leave it unset (or `false`) in production.
73
+
74
+ ---
75
+
76
+ ## All options
77
+
78
+ ```ts
79
+ interface OpenApiOptions {
80
+ enabled?: boolean // default: true when object is present
81
+ title?: string // default: 'Halifax API'
82
+ version?: string // default: '1.0.0'
83
+ description?: string // markdown supported
84
+ servers?: Array<{ url: string; description?: string }>
85
+ envelope?: string | null // mirrors CrudApiOptions.envelope — auto-propagated
86
+ specPath?: string // default: '/openapi.json'
87
+ docsPath?: string // default: '/docs'
88
+ }
89
+ ```
90
+
91
+ ### Custom paths
92
+
93
+ ```ts
94
+ openapi: {
95
+ specPath: '/spec/openapi.json',
96
+ docsPath: '/api-reference'
97
+ }
98
+ // → GET /api/v1/spec/openapi.json
99
+ // → GET /api/v1/api-reference
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Type introspection
105
+
106
+ ### Prisma resources (automatic)
107
+
108
+ When you use `PrismaAdapter` or `createPrismaResources`, Halifax reads the Prisma DMMF and maps types automatically:
109
+
110
+ | Prisma type | OpenAPI type | Format |
111
+ | ----------- | ------------ | ----------- |
112
+ | `String` | `string` | — |
113
+ | `Int` | `integer` | `int32` |
114
+ | `BigInt` | `integer` | `int64` |
115
+ | `Float` | `number` | `float` |
116
+ | `Decimal` | `number` | `double` |
117
+ | `Boolean` | `boolean` | — |
118
+ | `DateTime` | `string` | `date-time` |
119
+ | `Json` | `object` | — |
120
+ | `Bytes` | `string` | `binary` |
121
+
122
+ No configuration needed. Types flow through automatically.
123
+
124
+ ### Custom / non-Prisma repositories
125
+
126
+ Add optional `type` and `format` to any `FieldDefinition`:
127
+
128
+ ```ts
129
+ const orders: ResourceDefinition = {
130
+ routePrefix: 'orders',
131
+ repository: myCustomRepo,
132
+ fields: [
133
+ { name: 'id', type: 'integer', writable: false },
134
+ { name: 'total', type: 'number', format: 'double' },
135
+ { name: 'status', type: 'string' },
136
+ { name: 'paid', type: 'boolean' },
137
+ { name: 'createdAt', type: 'string', format: 'date-time', writable: false }
138
+ ]
139
+ }
140
+ ```
141
+
142
+ Fields without a `type` default to `string`.
143
+
144
+ ---
145
+
146
+ ## Generated endpoints
147
+
148
+ The spec documents exactly the operations your `permissions` allow. Disabled operations are omitted entirely.
149
+
150
+ ### Collection routes — `/{resource}`
151
+
152
+ | Method | Permission flag | Summary |
153
+ | -------- | ----------------- | ------------------------------------------ |
154
+ | `GET` | `allowReadMany` | List records (paginated, filtered, sorted) |
155
+ | `POST` | `allowCreate` | Create one or many records |
156
+ | `PATCH` | `allowUpdateMany` | Bulk-update matching records |
157
+ | `DELETE` | `allowDeleteMany` | Bulk-delete matching records |
158
+
159
+ ### Item routes — `/{resource}/{id}`
160
+
161
+ | Method | Permission flag | Summary |
162
+ | -------- | ---------------- | -------------------------- |
163
+ | `GET` | `allowReadOne` | Get one record by ID |
164
+ | `PATCH` | `allowUpdateOne` | Partially update a record |
165
+ | `PUT` | `allowUpsertOne` | Create or replace a record |
166
+ | `DELETE` | `allowDeleteOne` | Delete a record |
167
+
168
+ ### Query route — `/{resource}/query`
169
+
170
+ | Method | Permission flag | Summary |
171
+ | ------ | ------------------------------- | ---------------------- |
172
+ | `POST` | `allowReadManyWithQueryBuilder` | Advanced query builder |
173
+
174
+ ---
175
+
176
+ ## Query string parameters (GET endpoints)
177
+
178
+ Every list endpoint (`GET /{resource}`) documents the following parameters:
179
+
180
+ | Parameter | Type | Description |
181
+ | ------------------- | --------------- | -------------------------------------------------------------------------------------- |
182
+ | `limit` | integer | Maximum records to return (capped by `maxLimit`). |
183
+ | `offset` | integer | Records to skip for pagination. |
184
+ | `fields` | string | Comma-separated field names to include. Enum of available names is shown. |
185
+ | `order` | string | Sort expression: `field:asc` or `field:desc`, comma-separated. Sortable fields listed. |
186
+ | `include` | string | Comma-separated relation names to eager-load. Enum of available names is shown. |
187
+ | _filterable fields_ | varies | One query param per filterable field for simple equality filters. |
188
+ | `X-Correlation-ID` | string (header) | Optional. Echoed back in the response header for tracing. |
189
+
190
+ **Example:** `GET /posts?limit=20&offset=0&published=true&order=createdAt:desc&fields=id,title,createdAt`
191
+
192
+ > For range, LIKE, IN, or OR conditions, use `POST /{resource}/query` instead.
193
+
194
+ ---
195
+
196
+ ## Request / response shapes
197
+
198
+ ### List response (`GET` and `POST .../query`)
199
+
200
+ ```json
201
+ {
202
+ "count": 42,
203
+ "results": [{ "id": 1, "title": "Hello", "published": true }]
204
+ }
205
+ ```
206
+
207
+ `count` is the total matching rows before pagination. `results` is the current page.
208
+
209
+ ### Create (`POST /{resource}`)
210
+
211
+ Send a single object or an array:
212
+
213
+ ```json
214
+ { "title": "Hello", "published": false }
215
+ ```
216
+
217
+ ```json
218
+ [{ "title": "Hello" }, { "title": "World" }]
219
+ ```
220
+
221
+ Returns the created record(s) with HTTP `201`. Supports the `Idempotency-Key` header.
222
+
223
+ ### Update one (`PATCH /{resource}/{id}`)
224
+
225
+ Partial update — only the fields present in the body are changed:
226
+
227
+ ```json
228
+ { "published": true }
229
+ ```
230
+
231
+ Returns the updated record with HTTP `200`.
232
+
233
+ ### Upsert (`PUT /{resource}/{id}`)
234
+
235
+ Create or replace a record at the given ID. Always returns HTTP `200`.
236
+
237
+ ### Bulk update (`PATCH /{resource}`)
238
+
239
+ The body combines `QueryOptions` fields (to select records) with an `update` key (the fields to set):
240
+
241
+ ```json
242
+ {
243
+ "where": [{ "field": "status", "comparison": "=", "value": "draft" }],
244
+ "update": { "status": "archived" }
245
+ }
246
+ ```
247
+
248
+ `where` is **required** (at least one filter) — this prevents accidental full-table updates.
249
+
250
+ Response: `{ "updated": [<ids>], "results": [<records>] }`
251
+
252
+ ### Bulk delete (`DELETE /{resource}`)
253
+
254
+ The body is a `QueryOptions` object. `where` is **required**:
255
+
256
+ ```json
257
+ {
258
+ "where": [{ "field": "deletedAt", "comparison": "IS NOT NULL" }]
259
+ }
260
+ ```
261
+
262
+ Response: `{ "deleted": [<ids or records>] }`
263
+
264
+ ### Delete one (`DELETE /{resource}/{id}`)
265
+
266
+ Response: `{ "deleted": true }` with HTTP `200`.
267
+
268
+ ---
269
+
270
+ ## POST .../query — advanced query builder
271
+
272
+ The query builder endpoint accepts a `QueryOptions` body for full-featured filtering:
273
+
274
+ ```json
275
+ {
276
+ "where": [
277
+ { "field": "published", "comparison": "=", "value": true },
278
+ { "field": "createdAt", "comparison": ">=", "value": "2024-01-01T00:00:00Z" },
279
+ {
280
+ "operator": "OR",
281
+ "children": [
282
+ { "field": "title", "comparison": "CONTAINS", "value": "Halifax" },
283
+ { "field": "title", "comparison": "STARTS WITH", "value": "Hello" }
284
+ ]
285
+ }
286
+ ],
287
+ "orderBy": [{ "field": "createdAt", "direction": "desc" }],
288
+ "limit": 20,
289
+ "offset": 0,
290
+ "fields": ["id", "title", "createdAt"],
291
+ "include": ["author"]
292
+ }
293
+ ```
294
+
295
+ ### Supported comparison operators
296
+
297
+ | Operator | Description | Value format |
298
+ | ----------------- | ------------------------- | ------------------- |
299
+ | `=` | Equals | scalar |
300
+ | `<>` | Not equals | scalar |
301
+ | `<` `>` `<=` `>=` | Numeric / date comparison | scalar |
302
+ | `IN` | In list | `[val1, val2, ...]` |
303
+ | `NOT IN` | Not in list | `[val1, val2, ...]` |
304
+ | `BETWEEN` | Inclusive range | `[min, max]` |
305
+ | `NOT BETWEEN` | Outside range | `[min, max]` |
306
+ | `LIKE` | SQL LIKE (`%` wildcard) | string |
307
+ | `NOT LIKE` | SQL NOT LIKE | string |
308
+ | `CONTAINS` | Substring match | string |
309
+ | `STARTS WITH` | Prefix match | string |
310
+ | `ENDS WITH` | Suffix match | string |
311
+ | `IS NULL` | Null check | _(omit value)_ |
312
+ | `IS NOT NULL` | Not null check | _(omit value)_ |
313
+
314
+ ### AND / OR precedence
315
+
316
+ Flat `where` arrays use **AND-precedence over OR** (same as SQL). Use `children` groups with an `operator` for explicit parenthesisation:
317
+
318
+ ```json
319
+ {
320
+ "where": [
321
+ { "field": "active", "comparison": "=", "value": true },
322
+ {
323
+ "operator": "OR",
324
+ "children": [
325
+ { "field": "role", "comparison": "=", "value": "admin" },
326
+ { "field": "role", "comparison": "=", "value": "moderator" }
327
+ ]
328
+ }
329
+ ]
330
+ }
331
+ ```
332
+
333
+ Equivalent SQL: `WHERE active = true AND (role = 'admin' OR role = 'moderator')`
334
+
335
+ ---
336
+
337
+ ## Response envelopes
338
+
339
+ When `envelope` is configured on the router or per-resource, the spec reflects it:
340
+
341
+ ```ts
342
+ createExpressCrudRouter(resources, {
343
+ envelope: 'data',
344
+ openapi: { title: 'My API', version: '1.0.0' }
345
+ })
346
+ ```
347
+
348
+ All success response schemas automatically wrap under the envelope key:
349
+
350
+ ```json
351
+ { "data": { "id": 1, "title": "Hello" } }
352
+ ```
353
+
354
+ Per-resource `envelope` takes precedence over the API-wide default (including `null` to opt out).
355
+
356
+ ---
357
+
358
+ ## Error responses
359
+
360
+ All error responses share a consistent shape:
361
+
362
+ ```json
363
+ {
364
+ "errors": [{ "code": "NOT_FOUND", "message": "Not found." }]
365
+ }
366
+ ```
367
+
368
+ ### HTTP status codes
369
+
370
+ | Code | Meaning | When raised |
371
+ | ----- | ---------------------- | -------------------------------------------------------------------------------- |
372
+ | `400` | Bad Request | Invalid ID format, malformed query params or body, invalid filter expressions |
373
+ | `401` | Unauthorized | Missing or invalid auth credentials |
374
+ | `403` | Forbidden | Valid credentials but insufficient permissions |
375
+ | `404` | Not Found | Record with the given ID does not exist |
376
+ | `405` | Method Not Allowed | HTTP method not enabled for this resource |
377
+ | `406` | Not Acceptable | Client's `Accept` header excludes `application/json` |
378
+ | `415` | Unsupported Media Type | Body-carrying request with non-JSON `Content-Type` |
379
+ | `422` | Unprocessable Entity | Unknown fields in body, missing required filter, empty update payload |
380
+ | `500` | Internal Server Error | Unexpected repository or framework error |
381
+ | `501` | Not Implemented | Repository does not support this operation (e.g. upsert, updateMany, deleteMany) |
382
+
383
+ ---
384
+
385
+ ## Security schemes (Authorize button in Swagger UI)
386
+
387
+ Halifax automatically reads the security scheme from your `authStrategy` and wires it into the spec. The Swagger UI "Authorize" button appears pre-configured — no extra setup needed.
388
+
389
+ | Strategy | Scheme documented |
390
+ | ------------------------------------------ | ---------------------------------------------------- |
391
+ | `ApiKeyAuthStrategy` | `apiKey` in header (uses the configured header name) |
392
+ | `JwtClaimsAuthStrategy` | `http` bearer JWT |
393
+ | `PassportJwtStrategy` | `http` bearer JWT |
394
+ | `Auth0JwtStrategy` / `FirebaseJwtStrategy` | `http` bearer JWT |
395
+ | `PassportSessionStrategy` | `apiKey` in cookie (`connect.sid`) |
396
+ | `AllowAllAuthStrategy` / custom | No security requirement |
397
+
398
+ ### Custom strategy
399
+
400
+ Implement `openApiScheme()` on your custom strategy and it's picked up automatically:
401
+
402
+ ```ts
403
+ class MyHmacStrategy implements AuthStrategy {
404
+ async authenticate(req) {
405
+ /* ... */
406
+ }
407
+
408
+ openApiScheme(): SecurityScheme {
409
+ return { type: 'apiKey', in: 'header', name: 'X-Signature' }
410
+ }
411
+ }
412
+ ```
413
+
414
+ ### Override without touching the strategy
415
+
416
+ Pass `securityScheme` directly in `openapi` options to override whatever the strategy reports:
417
+
418
+ ```ts
419
+ openapi: {
420
+ title: 'My API',
421
+ securityScheme: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }
422
+ }
423
+ ```
424
+
425
+ ---
426
+
427
+ ## Using the spec programmatically
428
+
429
+ `generateOpenApiSpec` is exported and can be called standalone — no server needed:
430
+
431
+ ```ts
432
+ import { generateOpenApiSpec } from '@edium/halifax'
433
+
434
+ const spec = generateOpenApiSpec(resources, {
435
+ title: 'My API',
436
+ version: '1.0.0'
437
+ })
438
+
439
+ // Write to disk
440
+ import { writeFileSync } from 'fs'
441
+ writeFileSync('openapi.json', JSON.stringify(spec, null, 2))
442
+
443
+ // Or pass to a CI validation tool
444
+ import { validate } from '@redocly/openapi-core'
445
+ await validate({ document: spec })
446
+ ```
447
+
448
+ This is useful for:
449
+
450
+ - **CI spec validation** — catch breaking changes before deploy
451
+ - **Code generation** — pipe into `openapi-typescript`, `@hey-api/openapi-ts`, or `@edium/halifax-codegen`
452
+ - **Static hosting** — publish the spec alongside your deployed API
453
+
454
+ ---
455
+
456
+ ## Frequently asked questions
457
+
458
+ **Do I need to restart the server to update the spec?**
459
+ No. The spec is generated once at startup from the registered resource definitions. Restart is only needed when you change resource definitions in code.
460
+
461
+ **Does it work with Fastify / HyperExpress?**
462
+ The `openapi` option is on `CrudApiOptions` which is shared by all HTTP adapters. For non-Express adapters use `registerCrudApi` directly — the spec and docs routes are registered on whatever `HttpServer` adapter you provide.
463
+
464
+ **Can I add authentication to the docs route?**
465
+ The `/docs` and `/openapi.json` routes bypass your `authStrategy` since they're registered after the resource routes without the auth wrapper. To protect them, mount them behind your own middleware before calling `createExpressCrudRouter`.
466
+
467
+ **Why does the spec use `results` instead of `data` for list responses?**
468
+ `results` is the field name in Halifax's internal `ListResult` type. The spec accurately reflects what the API actually returns.
469
+
470
+ **Can I use this spec for client code generation?**
471
+ Yes. Feed `openapi.json` into any OpenAPI generator. Phase 4 of the Halifax roadmap (`@edium/halifax-codegen`) will generate a fully-typed TanStack Query client directly from this spec.
@@ -245,6 +245,83 @@ The integration suite runs unchanged against **all six** engines in CI — Postg
245
245
 
246
246
  **MongoDB note.** MongoDB is absent from the table above because **Prisma 7 dropped its MongoDB connector** (it's "coming soon" in v7) — and the table/CI matrix target Prisma 7. MongoDB still works **on Prisma 6**, which Halifax also supports (see the Prisma 6 section above) — Mongo keys are 24-character `ObjectId` strings (`@id @default(auto()) @map("_id") @db.ObjectId`), and Halifax's `:id` route validation already accepts integers, UUIDs, **and** ObjectIds, so id-based routes work on Mongo out of the box. The forward-ready `schema.mongodb.prisma` and an ObjectId-aware integration suite rejoin the CI matrix unchanged the moment Prisma 7 supports MongoDB again.
247
247
 
248
+ ## Drizzle ORM Adapter
249
+
250
+ `DrizzleAdapter` implements the full `Repository` interface against any Drizzle-compatible database. It lives behind a sub-path export so `drizzle-orm` is never a hard dependency when unused:
251
+
252
+ ```bash
253
+ pnpm add drizzle-orm
254
+ ```
255
+
256
+ ```ts
257
+ import { DrizzleAdapter } from '@edium/halifax/drizzle'
258
+ import { drizzle } from 'drizzle-orm/postgres-js'
259
+ import postgres from 'postgres'
260
+ import { usersTable } from './schema'
261
+
262
+ const db = drizzle(postgres(process.env.DATABASE_URL!))
263
+
264
+ const usersResource: ResourceDefinition = {
265
+ routePrefix: 'users',
266
+ repository: new DrizzleAdapter(db, usersTable)
267
+ // No `fields` needed — derived automatically from the table schema.
268
+ }
269
+ ```
270
+
271
+ ### Supported databases
272
+
273
+ `DrizzleAdapter` works with any driver Drizzle supports. The commonly used ones:
274
+
275
+ | Database | Drizzle driver import |
276
+ | -------------- | ---------------------------- |
277
+ | PostgreSQL | `drizzle-orm/postgres-js` |
278
+ | MySQL | `drizzle-orm/mysql2` |
279
+ | SQLite | `drizzle-orm/better-sqlite3` |
280
+ | LibSQL / Turso | `drizzle-orm/libsql` |
281
+
282
+ ### Type introspection
283
+
284
+ `DrizzleAdapter` calls `getTableColumns()` on your table and derives the Halifax field schema automatically — types, primary key, and `writable` flags are all inferred. For OpenAPI generation, Drizzle column types are mapped to their OpenAPI equivalents:
285
+
286
+ | Drizzle `dataType` | OpenAPI type | Format |
287
+ | ------------------ | ------------ | ----------- |
288
+ | `string` | `string` | — |
289
+ | `number` | `number` | — |
290
+ | `boolean` | `boolean` | — |
291
+ | `bigint` | `integer` | `int64` |
292
+ | `date` | `string` | `date-time` |
293
+ | `json` | `object` | — |
294
+ | `buffer` | `string` | `binary` |
295
+
296
+ ### Constructor options
297
+
298
+ ```ts
299
+ new DrizzleAdapter(db, table, config?, scope?)
300
+ ```
301
+
302
+ | Parameter | Type | Description |
303
+ | ---------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
304
+ | `db` | Drizzle DB instance | Any Drizzle-compatible database connection. |
305
+ | `table` | Drizzle `Table` | Your table schema (e.g. `usersTable`). |
306
+ | `config.idField` | `string` (optional) | Primary key field name. Defaults to auto-detecting the first column marked `.primaryKey()`. Set explicitly for composite PKs or non-standard names. |
307
+ | `scope` | `TenantScope \| null` | Tenant scope. Set by `withScope()` internally — do not pass directly. |
308
+
309
+ ### Multi-tenancy
310
+
311
+ `DrizzleAdapter` supports per-resource tenant scoping via `withScope()` exactly like `PrismaAdapter`. See [README_MULTITENANCY.md](./README_MULTITENANCY.md) for how to configure it on the resource.
312
+
313
+ ### Static field derivation
314
+
315
+ You can derive the field schema without constructing a full adapter instance:
316
+
317
+ ```ts
318
+ import { DrizzleAdapter } from '@edium/halifax/drizzle'
319
+ import { usersTable } from './schema'
320
+
321
+ const fields = DrizzleAdapter.fieldsFromTable(usersTable)
322
+ // Use as the `fields` array in a ResourceDefinition, or inspect for overrides.
323
+ ```
324
+
248
325
  ## Targeting database Views
249
326
 
250
327
  A database **view is just a model** to Halifax. Prisma exposes a `view` block as a delegate with the same read API as a model (`prisma.activeUsers.findMany()`), so you point a resource's repository at it and disable writes:
@@ -0,0 +1,114 @@
1
+ # Types — `@edium/halifax`
2
+
3
+ All publicly exported TypeScript type aliases, enums, and constants. Interfaces are in [README_INTERFACES.md](./README_INTERFACES.md); classes are in [README_CLASSES.md](./README_CLASSES.md).
4
+
5
+ ---
6
+
7
+ ## Type Aliases
8
+
9
+ ### Core / Resource
10
+
11
+ | Type | Import path | Definition | Description |
12
+ | ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
13
+ | `CrudAction` | `@edium/halifax` | `'create' \| 'readOne' \| 'readMany' \| 'readManyWithQueryBuilder' \| 'updateOne' \| 'updateMany' \| 'upsertOne' \| 'deleteOne' \| 'deleteMany'` | Identifies a single CRUD operation. Used in `requiredPermissions` maps and `AuthorizeParams`. |
14
+ | `FieldType` | `@edium/halifax` | `'string' \| 'integer' \| 'number' \| 'boolean' \| 'object'` | OpenAPI-compatible scalar type for a field. Set on `FieldDefinition.type` for custom repos; auto-populated by Prisma and Drizzle adapters. |
15
+ | `HttpMethod` | `@edium/halifax` | `'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE' \| '*'` | HTTP methods supported by Halifax routes. `'*'` is used for 405 catch-all handlers. |
16
+ | `HttpRouteHandler` | `@edium/halifax` | `(req: HttpRequest, res: HttpResponse) => Promise<void> \| void` | Function signature for framework-agnostic route handlers passed to `HttpServer.registerRoute`. |
17
+
18
+ ### Auth
19
+
20
+ | Type | Import path | Definition | Description |
21
+ | ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
22
+ | `SecurityScheme` | `@edium/halifax` | `{ type: 'apiKey'; in: 'header' \| 'query' \| 'cookie'; name: string } \| { type: 'http'; scheme: 'bearer' \| 'basic' }` | OpenAPI 3.1 security scheme descriptor returned by `AuthStrategy.openApiScheme()`. Used to populate the Swagger UI "Authorize" button. |
23
+
24
+ ### Response envelopes
25
+
26
+ | Type | Import path | Definition | Description |
27
+ | --------------------------- | ---------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------- |
28
+ | `ListResult<TRecord>` | `@edium/halifax` | `{ count: number; results: TRecord[] }` | Returned by `getMany` and `executeQuery`. `count` is the total matching rows before pagination. |
29
+ | `QueryResult<TRecord>` | `@edium/halifax` | `{ count?: number; results: TRecord[] }` | Returned by `executeQuery` (query-builder endpoint). `count` is optional. |
30
+ | `UpdateManyResult<TRecord>` | `@edium/halifax` | `{ updated: unknown[]; results?: TRecord[] }` | Returned by `updateMany`. `updated` is the list of affected IDs. |
31
+ | `DeleteManyResult` | `@edium/halifax` | `{ deleted: unknown[] }` | Returned by `deleteMany`. `deleted` is the list of affected IDs. |
32
+
33
+ ### Query AST
34
+
35
+ | Type | Import path | Definition | Description |
36
+ | ------------- | ---------------- | ------------------------------------- | ----------------------------------------------------------------------- |
37
+ | `QueryScalar` | `@edium/halifax` | `string \| number \| boolean \| null` | Scalar value accepted by filter comparison operators in `IQueryFilter`. |
38
+
39
+ ### HTTP adapter type aliases
40
+
41
+ These are aliased to `CrudApiOptions` — documented in [README_INTERFACES.md](./README_INTERFACES.md).
42
+
43
+ | Type | Import path | Description |
44
+ | ---------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------- |
45
+ | `ExpressCrudRouterOptions` | `@edium/halifax` | Alias of `CrudApiOptions`. The options type accepted by `createExpressCrudRouter`. |
46
+ | `FastifyCrudPluginOptions` | `@edium/halifax` | Alias of `CrudApiOptions`. The options type accepted by `createFastifyCrudPlugin`. |
47
+ | `FastifyCrudPlugin` | `@edium/halifax` | `(instance: FastifyAppLike) => Promise<void>`. Type of the Fastify plugin returned by `createFastifyCrudPlugin`. |
48
+ | `HyperExpressCrudRouterOptions` | `@edium/halifax` | Alias of `CrudApiOptions`. The options type accepted by `createHyperExpressCrudRouter`. |
49
+ | `UltimateExpressCrudRouterOptions` | `@edium/halifax` | Alias of `CrudApiOptions`. The options type accepted by `createUltimateExpressCrudRouter`. |
50
+
51
+ ### Drizzle sub-path (`@edium/halifax/drizzle`)
52
+
53
+ | Type | Import path | Definition | Description |
54
+ | -------------- | ------------------------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
55
+ | `AnyDrizzleDB` | `@edium/halifax/drizzle` | `any` | A Drizzle database connection of any driver type. Typed as `any` because Drizzle does not export a single unified DB type across drivers. |
56
+ | `ColumnMap` | `@edium/halifax/drizzle` | `Record<string, AnyColumn>` | Map from column name to Drizzle `AnyColumn`, used internally by the AST compiler. Useful when writing custom Drizzle query extensions. |
57
+
58
+ ---
59
+
60
+ ## Enums
61
+
62
+ All three enums are re-exported from `@edium/halifax-types` through the main `@edium/halifax` entry point.
63
+
64
+ ### `SqlComparison`
65
+
66
+ Comparison operators used in `IQueryFilter.comparison`. Accepted by the query-builder endpoint (`POST /:resource/query`) and `QueryBuilder` methods.
67
+
68
+ | Value | Operator | Notes |
69
+ | -------------------- | ------------- | ------------------------- |
70
+ | `Equal` | `=` | |
71
+ | `NotEqual` | `<>` | |
72
+ | `LessThan` | `<` | |
73
+ | `LessThanOrEqual` | `<=` | |
74
+ | `GreaterThan` | `>` | |
75
+ | `GreaterThanOrEqual` | `>=` | |
76
+ | `In` | `IN` | `value1` must be an array |
77
+ | `NotIn` | `NOT IN` | `value1` must be an array |
78
+ | `Between` | `BETWEEN` | Use `value1` and `value2` |
79
+ | `NotBetween` | `NOT BETWEEN` | Use `value1` and `value2` |
80
+ | `Like` | `LIKE` | `%` wildcard |
81
+ | `NotLike` | `NOT LIKE` | |
82
+ | `Contains` | `CONTAINS` | Substring match |
83
+ | `StartsWith` | `STARTS WITH` | Prefix match |
84
+ | `EndsWith` | `ENDS WITH` | Suffix match |
85
+ | `IsNull` | `IS NULL` | Omit `value1` |
86
+ | `IsNotNull` | `IS NOT NULL` | Omit `value1` |
87
+
88
+ ### `SqlOperator`
89
+
90
+ Logical operator that joins a filter condition to the next sibling condition in a `where` array.
91
+
92
+ | Value | Description |
93
+ | ----- | -------------------------------------------- |
94
+ | `And` | `AND` — the next condition must also be true |
95
+ | `Or` | `OR` — the next condition is an alternative |
96
+
97
+ ### `SqlOrder`
98
+
99
+ Sort direction for `ISort.order` and `QueryBuilder.orderBy`.
100
+
101
+ | Value | Direction |
102
+ | ------ | ------------------- |
103
+ | `ASC` | Ascending (default) |
104
+ | `DESC` | Descending |
105
+
106
+ ---
107
+
108
+ ## Constants
109
+
110
+ | Constant | Import path | Value | Description |
111
+ | ------------------------ | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
112
+ | `DEFAULT_PAGE_LIMIT` | `@edium/halifax` | `5000` | Default page size applied when a resource sets no `defaultLimit` and the caller omits `?limit=`. Set `defaultLimit: 0` on a resource to disable. |
113
+ | `MAX_PAGE_LIMIT` | `@edium/halifax` | `5000` | Default hard cap on page size. Requests above this are silently capped. Set `maxLimit: 0` on a resource to remove the cap. |
114
+ | `defaultCrudPermissions` | `@edium/halifax` | All actions `true` | The `CrudPermissions` object applied to every resource — all nine CRUD actions enabled. Override per-resource with `permissions`. |