@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.
- package/CHANGELOG.md +78 -1
- package/README.md +102 -17
- package/README_AUTH.md +38 -0
- package/README_AUTOCRUD.md +33 -0
- 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 +16 -16
- package/dist/core/crudRouter.js +98 -368
- 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 +48 -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
|
@@ -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.
|
package/README_REPO_ADAPTERS.md
CHANGED
|
@@ -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:
|
package/README_TYPES.md
ADDED
|
@@ -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`. |
|