@edium/halifax 2.1.0 → 2.2.1

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 +73 -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,217 @@
1
+ import type { AuthContext } from '../auth/AuthStrategy.js';
2
+ import type { HttpRequest, ListOptions, ListResult, QueryResult, UpdateManyResult, DeleteManyResult, ResourceDefinition } from '../core/types.js';
3
+ import type { IQueryOptions } from '@edium/halifax-types';
4
+ /** Synchronous or asynchronous return — hook functions may return either. */
5
+ export type MaybePromise<T> = T | Promise<T>;
6
+ /**
7
+ * Context object passed to every hook function. Provides the caller's resolved auth, the
8
+ * normalized resource definition, and the raw HTTP request so hooks have access to headers,
9
+ * IP addresses, and any properties set by upstream framework middleware.
10
+ */
11
+ export interface HookContext {
12
+ /** Resolved identity and permissions for the current caller. */
13
+ auth: AuthContext;
14
+ /** The normalized resource being accessed (name, fields, routePrefix, etc.). */
15
+ resource: ResourceDefinition;
16
+ /** The incoming HTTP request — access headers, the raw framework request, etc. */
17
+ req: HttpRequest;
18
+ }
19
+ /**
20
+ * Lifecycle hooks for a single Halifax resource.
21
+ *
22
+ * Attach these under `ResourceDefinition.hooks` to inject custom logic before or after
23
+ * any CRUD operation without writing a custom repository or HTTP middleware.
24
+ *
25
+ * **Before hooks** receive the incoming data and can:
26
+ * - Return a **modified copy** to replace the payload going into the database.
27
+ * - Return **`void` / `undefined`** to leave the data unchanged.
28
+ * - **Throw** any error to abort the operation — Halifax catches it and sends the
29
+ * appropriate HTTP error response. Use Halifax's own error classes
30
+ * (`AuthorizationError`, `BadRequestError`, `UnprocessableEntityError`, …) for
31
+ * precise status codes.
32
+ *
33
+ * **After hooks** receive the outgoing result and can:
34
+ * - Return a **modified copy** to replace what is sent to the client.
35
+ * - Return **`void` / `undefined`** to leave the result unchanged.
36
+ * - **Throw** to replace a successful DB result with an error response (rare but valid).
37
+ *
38
+ * After hooks run after the database write but **before** response field-filtering
39
+ * (`readRoles` / `selectable`). The hook therefore sees every field the DB returned, not
40
+ * just the fields the caller is allowed to read.
41
+ *
42
+ * Method syntax is intentional — TypeScript applies bivariant parameter checking to
43
+ * interface methods, which allows a typed `CrudHooks<Post>` to be assigned to an
44
+ * untyped `CrudHooks` (the generic defaults to `unknown`) without a cast.
45
+ *
46
+ * @template TRecord Shape of a full record returned by the server.
47
+ * @template TCreate Shape of a create payload.
48
+ * @template TUpdate Shape of an update payload.
49
+ *
50
+ * @example Stamp audit fields and emit events
51
+ * ```ts
52
+ * const posts: ResourceDefinition = {
53
+ * routePrefix: 'posts',
54
+ * repository: new PrismaAdapter({ delegate: prisma.post }),
55
+ * hooks: {
56
+ * beforeCreate: (data, { auth }) => ({ ...data, createdBy: auth.userId }),
57
+ * afterCreate: async (result) => { await events.emit('post.created', result) },
58
+ * beforeUpdateOne: (id, data, { auth }) => ({ ...data, updatedBy: auth.userId }),
59
+ * }
60
+ * }
61
+ * ```
62
+ */
63
+ export interface CrudHooks<TRecord = unknown, TCreate = Partial<TRecord>, TUpdate = Partial<TRecord>> {
64
+ /**
65
+ * Fired once **per record** before it is inserted — for both single-object and array POST bodies.
66
+ *
67
+ * Return a modified payload to replace the incoming data, or `void` to use as-is.
68
+ * Throw to abort the entire create operation.
69
+ *
70
+ * @example Stamp `createdBy` from the auth context:
71
+ * ```ts
72
+ * beforeCreate: (data, { auth }) => ({ ...data, createdBy: auth.userId })
73
+ * ```
74
+ */
75
+ beforeCreate?(data: TCreate, ctx: HookContext): MaybePromise<TCreate | void>;
76
+ /**
77
+ * Fired once **per record** after it is inserted — for both single and bulk POST.
78
+ *
79
+ * Return a modified record to replace what is sent to the client, or `void` to use as-is.
80
+ * Readable-field filtering (`readRoles` / `selectable`) is applied after this hook.
81
+ *
82
+ * @example Emit a domain event:
83
+ * ```ts
84
+ * afterCreate: async (result, { resource }) => {
85
+ * await events.emit(`${resource.name}.created`, result)
86
+ * }
87
+ * ```
88
+ */
89
+ afterCreate?(result: TRecord, ctx: HookContext): MaybePromise<TRecord | void>;
90
+ /**
91
+ * Fired before a single record is fetched by ID (`GET /resource/:id`).
92
+ *
93
+ * Cannot modify the ID — use this for additional ownership/authorization checks or
94
+ * audit logging. Throw to block the read.
95
+ *
96
+ * @example Block reads on records not owned by the caller:
97
+ * ```ts
98
+ * beforeReadOne: async (id, { auth }) => {
99
+ * const record = await db.find(id)
100
+ * if (record.ownerId !== auth.userId) throw new AuthorizationError()
101
+ * }
102
+ * ```
103
+ */
104
+ beforeReadOne?(id: string | number, ctx: HookContext): MaybePromise<void>;
105
+ /**
106
+ * Fired after a single record is fetched.
107
+ * Return a modified record or `void` to use as-is (e.g. attach computed / virtual fields).
108
+ */
109
+ afterReadOne?(result: TRecord, ctx: HookContext): MaybePromise<TRecord | void>;
110
+ /**
111
+ * Fired before a paginated list query (`GET /resource`).
112
+ *
113
+ * Return modified `ListOptions` to adjust pagination, inject extra filters, or force a
114
+ * sort — or `void` to leave the options unchanged.
115
+ *
116
+ * @example Restrict results to the caller's own records:
117
+ * ```ts
118
+ * beforeReadMany: (options, { auth }) => ({
119
+ * ...options,
120
+ * where: { ...options.where, ownerId: auth.userId }
121
+ * })
122
+ * ```
123
+ */
124
+ beforeReadMany?(options: ListOptions, ctx: HookContext): MaybePromise<ListOptions | void>;
125
+ /**
126
+ * Fired after a list query.
127
+ * Return a modified `ListResult` or `void` (e.g. annotate each record or replace `count`).
128
+ */
129
+ afterReadMany?(result: ListResult<TRecord>, ctx: HookContext): MaybePromise<ListResult<TRecord> | void>;
130
+ /**
131
+ * Fired before a single-record update (`PATCH /resource/:id`).
132
+ * Return modified data or `void`. Throw to abort.
133
+ *
134
+ * @example Stamp `updatedBy` and `updatedAt`:
135
+ * ```ts
136
+ * beforeUpdateOne: (id, data, { auth }) => ({
137
+ * ...data,
138
+ * updatedBy: auth.userId,
139
+ * updatedAt: new Date().toISOString()
140
+ * })
141
+ * ```
142
+ */
143
+ beforeUpdateOne?(id: string | number, data: TUpdate, ctx: HookContext): MaybePromise<TUpdate | void>;
144
+ /**
145
+ * Fired after a single-record update.
146
+ * Return a modified record or `void` to use as-is.
147
+ */
148
+ afterUpdateOne?(result: TRecord, ctx: HookContext): MaybePromise<TRecord | void>;
149
+ /**
150
+ * Fired before a bulk update (`PATCH /resource` with a query body).
151
+ *
152
+ * Use for authorization checks or audit logging. Throw to abort.
153
+ * The query and update data cannot be modified from this hook — throw if the operation
154
+ * should be blocked, or use a custom repository wrapper for query rewriting.
155
+ */
156
+ beforeUpdateMany?(query: IQueryOptions, data: TUpdate, ctx: HookContext): MaybePromise<void>;
157
+ /**
158
+ * Fired after a bulk update.
159
+ * Return a modified `UpdateManyResult` or `void` to use as-is.
160
+ */
161
+ afterUpdateMany?(result: UpdateManyResult<TRecord>, ctx: HookContext): MaybePromise<UpdateManyResult<TRecord> | void>;
162
+ /**
163
+ * Fired before an upsert (`PUT /resource/:id`).
164
+ * Return modified data or `void`. Throw to abort.
165
+ */
166
+ beforeUpsertOne?(id: string | number, data: TCreate & TUpdate, ctx: HookContext): MaybePromise<(TCreate & TUpdate) | void>;
167
+ /**
168
+ * Fired after an upsert.
169
+ * Return a modified record or `void` to use as-is.
170
+ */
171
+ afterUpsertOne?(result: TRecord, ctx: HookContext): MaybePromise<TRecord | void>;
172
+ /**
173
+ * Fired before a single-record delete (`DELETE /resource/:id`).
174
+ *
175
+ * Use for cascades, soft-delete interceptors, or authorization checks. Throw to abort.
176
+ * The record still exists in the database when this hook fires.
177
+ */
178
+ beforeDeleteOne?(id: string | number, ctx: HookContext): MaybePromise<void>;
179
+ /**
180
+ * Fired after a single-record delete.
181
+ * The record is already gone from the database. Use for cleanup, event emission, or audit.
182
+ */
183
+ afterDeleteOne?(id: string | number, ctx: HookContext): MaybePromise<void>;
184
+ /**
185
+ * Fired before a bulk delete (`DELETE /resource` with a query body).
186
+ * Use for authorization checks or audit logging. Throw to abort.
187
+ */
188
+ beforeDeleteMany?(query: IQueryOptions, ctx: HookContext): MaybePromise<void>;
189
+ /**
190
+ * Fired after a bulk delete.
191
+ * Return a modified `DeleteManyResult` or `void` to use as-is.
192
+ */
193
+ afterDeleteMany?(result: DeleteManyResult, ctx: HookContext): MaybePromise<DeleteManyResult | void>;
194
+ /**
195
+ * Fired before a query-builder execution (`POST /resource/query`).
196
+ *
197
+ * Return a modified `IQueryOptions` to augment or restrict the query (e.g. inject a
198
+ * mandatory tenant filter that the client cannot remove), or `void` to use as-is.
199
+ *
200
+ * @example Inject a mandatory filter the client cannot remove:
201
+ * ```ts
202
+ * beforeQuery: (query, { auth }) => ({
203
+ * ...query,
204
+ * where: [
205
+ * ...(query.where ?? []),
206
+ * { field: 'tenantId', comparison: '=', value: auth.tenantId, operator: 'AND' }
207
+ * ]
208
+ * })
209
+ * ```
210
+ */
211
+ beforeQuery?(query: IQueryOptions, ctx: HookContext): MaybePromise<IQueryOptions | void>;
212
+ /**
213
+ * Fired after a query-builder execution.
214
+ * Return a modified `QueryResult` or `void` to use as-is.
215
+ */
216
+ afterQuery?(result: QueryResult<TRecord>, ctx: HookContext): MaybePromise<QueryResult<TRecord> | void>;
217
+ }
@@ -1,4 +1,4 @@
1
- import { SqlOrder } from '../enums/SqlOrder.js';
1
+ import { SqlOrder } from '@edium/halifax-types';
2
2
  import { UnprocessableEntityError } from '../errors/UnprocessableEntityError.js';
3
3
  import { DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT } from '../core/types.js';
4
4
  import { isValidInt32, validateFields, validateIncludes, validateQueryString, validateSelectableFields, validateSortableFields } from '../core/validation.js';
@@ -1,4 +1,6 @@
1
- import type { IQueryOptions } from '../interfaces/IQueryOptions.js';
1
+ import type { IQueryOptions, ListResult, QueryResult, UpdateManyResult, DeleteManyResult } from '@edium/halifax-types';
2
+ import type { CrudHooks } from '../core/hooks.js';
3
+ export type { ListResult, QueryResult, UpdateManyResult, DeleteManyResult };
2
4
  /** HTTP methods supported by Halifax routes. `'*'` matches any method (used for 405 fallbacks). */
3
5
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | '*';
4
6
  /** Framework-agnostic representation of an incoming HTTP request. */
@@ -84,32 +86,6 @@ export interface ListOptions {
84
86
  /** Relation names to eager-load. */
85
87
  include?: string[] | undefined;
86
88
  }
87
- /** Paginated result envelope returned by `getMany`. */
88
- export interface ListResult<TRecord> {
89
- /** Total number of matching records (before pagination). */
90
- count: number;
91
- /** Records for the current page. */
92
- results: TRecord[];
93
- }
94
- /** Result envelope returned by `deleteMany`. */
95
- export interface DeleteManyResult {
96
- /** IDs (or records) of the deleted rows. */
97
- deleted: unknown[];
98
- }
99
- /** Result envelope returned by `updateMany`. */
100
- export interface UpdateManyResult<TRecord> {
101
- /** IDs of the updated rows. */
102
- updated: unknown[];
103
- /** Updated records, when the adapter supports returning them. */
104
- results?: TRecord[];
105
- }
106
- /** Result envelope returned by `executeQuery`. */
107
- export interface QueryResult<TRecord> {
108
- /** Total number of matching records (before pagination). */
109
- count?: number;
110
- /** Records for the current page. */
111
- results: TRecord[];
112
- }
113
89
  /** Options passed to `createOne` / `createMany` for idempotent writes. */
114
90
  export interface CreateOptions {
115
91
  /** An idempotency key — the adapter may de-duplicate requests with the same key. */
@@ -248,6 +224,8 @@ export interface ModelField {
248
224
  isReadOnly: boolean;
249
225
  /** True when Prisma provides a default value (e.g. `@default(autoincrement())`). */
250
226
  hasDefault: boolean;
227
+ /** Prisma scalar type name (e.g. `'String'`, `'Int'`, `'Boolean'`, `'DateTime'`). Used for OpenAPI type inference. */
228
+ type?: string;
251
229
  }
252
230
  /** Minimal shape of a Prisma DMMF model — structurally compatible with `Prisma.DMMF.Model`. */
253
231
  export interface ModelSchema {
@@ -277,9 +255,14 @@ export interface ModelResourceOptions {
277
255
  defaultLimit?: number;
278
256
  /** Hard cap on page size. Requests above this are silently capped. */
279
257
  maxLimit?: number;
280
- /** Maximum nesting depth for WHERE clause children (default: 3). */
258
+ /** Maximum nesting depth for WHERE clause children (default: 4). */
281
259
  maxFilterDepth?: number;
282
260
  }
261
+ /**
262
+ * OpenAPI-compatible scalar type for a field. Used for spec generation only — has no effect
263
+ * on runtime behaviour. Auto-populated by `PrismaAdapter`; set manually for custom repositories.
264
+ */
265
+ export type FieldType = 'string' | 'integer' | 'number' | 'boolean' | 'object';
283
266
  /**
284
267
  * Describes a single column exposed through the Halifax API.
285
268
  *
@@ -299,6 +282,24 @@ export interface FieldDefinition {
299
282
  selectable?: boolean;
300
283
  /** When `false`, the field is stripped from POST/PATCH/PUT bodies. Defaults to `true` (except the primary key). */
301
284
  writable?: boolean;
285
+ /** OpenAPI scalar type. Auto-populated from Prisma DMMF; set manually for non-Prisma fields. Defaults to `'string'`. */
286
+ type?: FieldType;
287
+ /** OpenAPI format modifier (e.g. `'date-time'`, `'int64'`, `'binary'`). Auto-populated from Prisma DMMF. */
288
+ format?: string;
289
+ /**
290
+ * Roles or permissions required to **read** this field. Any single match grants access.
291
+ * When absent or empty, any authenticated caller can read the field (no restriction).
292
+ * Values are matched against `AuthContext.roles` and `AuthContext.permissions`.
293
+ */
294
+ readRoles?: string[];
295
+ /**
296
+ * Roles or permissions required to **write** this field. Any single match grants access.
297
+ * Fields the caller cannot write are silently dropped from POST/PATCH/PUT bodies
298
+ * (consistent with how `writable: false` behaves). When absent or empty, any caller
299
+ * with general write access can write this field.
300
+ * Values are matched against `AuthContext.roles` and `AuthContext.permissions`.
301
+ */
302
+ writeRoles?: string[];
302
303
  }
303
304
  /**
304
305
  * Declares that a resource is tenant-scoped: every request is confined to rows whose
@@ -371,8 +372,16 @@ export interface ResourceDefinition<TRecord = unknown, TCreate = Partial<TRecord
371
372
  * the cap entirely — combine `defaultLimit: 0` and `maxLimit: 0` to disable pagination.
372
373
  */
373
374
  maxLimit?: number;
374
- /** Maximum nesting depth for WHERE clause children. Defaults to 3. */
375
+ /** Maximum nesting depth for WHERE clause children. Defaults to 4. */
375
376
  maxFilterDepth?: number;
377
+ /**
378
+ * Lifecycle hooks for this resource. Halifax calls these before and after every CRUD
379
+ * operation, letting you inject custom logic — validation, auditing, event emission,
380
+ * data transformation — without writing a custom repository or HTTP middleware.
381
+ *
382
+ * See {@link CrudHooks} for the full list of available hooks and their signatures.
383
+ */
384
+ hooks?: CrudHooks<TRecord, TCreate, TUpdate>;
376
385
  /**
377
386
  * Read-through caching for this resource. When set, the router caches
378
387
  * `getOne`/`getMany`/query reads and invalidates them on any write to this resource.
@@ -1,5 +1,4 @@
1
- import type { IQueryFilter } from '../interfaces/IQueryFilter.js';
2
- import type { IQueryOptions } from '../interfaces/IQueryOptions.js';
1
+ import type { IQueryFilter, IQueryOptions } from '@edium/halifax-types';
3
2
  import type { ResourceDefinition } from '../core/types.js';
4
3
  /**
5
4
  * Returns `true` when `value` is a safe 32-bit integer >= `min`.
@@ -1,7 +1,5 @@
1
1
  import { validate as uuidValidate } from 'uuid';
2
- import { SqlComparison } from '../enums/SqlComparison.js';
3
- import { SqlOperator } from '../enums/SqlOperator.js';
4
- import { SqlOrder } from '../enums/SqlOrder.js';
2
+ import { SqlComparison, SqlOperator, SqlOrder } from '@edium/halifax-types';
5
3
  import { BadRequestError } from '../errors/BadRequestError.js';
6
4
  import { UnprocessableEntityError } from '../errors/UnprocessableEntityError.js';
7
5
  const reservedQueryStringProperties = ['fields', 'limit', 'offset', 'order', 'include'];
package/dist/index.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  export * from './adapters/http/ExpressAdapter.js';
2
+ export * from './openapi/index.js';
2
3
  export * from './adapters/orm/prisma/index.js';
3
4
  export * from './auth/AuthStrategy.js';
4
5
  export * from './core/cache/index.js';
5
6
  export * from './core/crudRouter.js';
7
+ export * from './core/hooks.js';
6
8
  export * from './core/queryString.js';
7
9
  export * from './core/types.js';
8
10
  export * from './core/validation.js';
9
- export * from './enums/SqlComparison.js';
10
- export * from './enums/SqlOperator.js';
11
- export * from './enums/SqlOrder.js';
11
+ export * from '@edium/halifax-types';
12
12
  export * from './errors/AuthenticationError.js';
13
13
  export * from './errors/AuthorizationError.js';
14
14
  export * from './errors/BadRequestError.js';
@@ -20,6 +20,3 @@ export * from './errors/NotImplementedError.js';
20
20
  export * from './errors/ServerError.js';
21
21
  export * from './errors/UnprocessableEntityError.js';
22
22
  export * from './errors/UnsupportedMediaTypeError.js';
23
- export * from './interfaces/IQueryFilter.js';
24
- export * from './interfaces/IQueryOptions.js';
25
- export * from './interfaces/ISort.js';
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  export * from './adapters/http/ExpressAdapter.js';
2
+ export * from './openapi/index.js';
2
3
  export * from './adapters/orm/prisma/index.js';
3
4
  export * from './auth/AuthStrategy.js';
4
5
  export * from './core/cache/index.js';
5
6
  export * from './core/crudRouter.js';
7
+ export * from './core/hooks.js';
6
8
  export * from './core/queryString.js';
7
9
  export * from './core/types.js';
8
10
  export * from './core/validation.js';
9
- export * from './enums/SqlComparison.js';
10
- export * from './enums/SqlOperator.js';
11
- export * from './enums/SqlOrder.js';
11
+ export * from '@edium/halifax-types';
12
12
  export * from './errors/AuthenticationError.js';
13
13
  export * from './errors/AuthorizationError.js';
14
14
  export * from './errors/BadRequestError.js';
@@ -20,6 +20,3 @@ export * from './errors/NotImplementedError.js';
20
20
  export * from './errors/ServerError.js';
21
21
  export * from './errors/UnprocessableEntityError.js';
22
22
  export * from './errors/UnsupportedMediaTypeError.js';
23
- export * from './interfaces/IQueryFilter.js';
24
- export * from './interfaces/IQueryOptions.js';
25
- export * from './interfaces/ISort.js';
@@ -0,0 +1 @@
1
+ export declare function generateDocsHtml(specPath: string, docsPath: string): string;
@@ -0,0 +1,47 @@
1
+ export function generateDocsHtml(specPath, docsPath) {
2
+ // Build a browser-relative URL so it works at any router mount prefix.
3
+ const specParts = specPath.replace(/^\//, '').split('/');
4
+ const docsDirParts = docsPath.replace(/^\//, '').split('/').slice(0, -1);
5
+ const specFilename = specParts[specParts.length - 1] ?? 'openapi.json';
6
+ const specDirParts = specParts.slice(0, -1);
7
+ let common = 0;
8
+ while (common < specDirParts.length &&
9
+ common < docsDirParts.length &&
10
+ specDirParts[common] === docsDirParts[common])
11
+ common++;
12
+ const ups = docsDirParts.length - common;
13
+ const downs = specDirParts.slice(common);
14
+ const upSegments = new Array(ups).fill('..');
15
+ const relSpecUrl = [...upSegments, ...downs, specFilename].join('/') || specFilename;
16
+ return `<!DOCTYPE html>
17
+ <html lang="en">
18
+ <head>
19
+ <meta charset="utf-8" />
20
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
21
+ <title>API Docs</title>
22
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
23
+ <style>
24
+ body { margin: 0; }
25
+ .swagger-ui .topbar { display: none; }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <div id="swagger-ui"></div>
30
+ <script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
31
+ <script>
32
+ window.onload = function () {
33
+ SwaggerUIBundle({
34
+ url: new URL('${relSpecUrl}', window.location.href).href,
35
+ dom_id: '#swagger-ui',
36
+ presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
37
+ layout: 'BaseLayout',
38
+ deepLinking: true,
39
+ tryItOutEnabled: true,
40
+ persistAuthorization: true,
41
+ filter: true
42
+ })
43
+ }
44
+ </script>
45
+ </body>
46
+ </html>`;
47
+ }
@@ -0,0 +1,3 @@
1
+ export { generateOpenApiSpec } from './specGenerator.js';
2
+ export { generateDocsHtml } from './generateDocsHtml.js';
3
+ export type { OpenApiOptions } from './specGenerator.js';
@@ -0,0 +1,2 @@
1
+ export { generateOpenApiSpec } from './specGenerator.js';
2
+ export { generateDocsHtml } from './generateDocsHtml.js';
@@ -0,0 +1,149 @@
1
+ import { type ResourceDefinition } from '../core/types.js';
2
+ import type { SecurityScheme } from '../auth/AuthStrategy.js';
3
+ /** Options for OpenAPI spec generation and the built-in docs UI. */
4
+ export interface OpenApiOptions {
5
+ /**
6
+ * Set `false` to disable OpenAPI entirely (routes not registered, spec not generated).
7
+ * Useful for conditionally enabling in non-production environments:
8
+ *
9
+ * ```ts
10
+ * openapi: {
11
+ * enabled: process.env.NODE_ENV !== 'production',
12
+ * title: 'My API'
13
+ * }
14
+ * ```
15
+ *
16
+ * Defaults to `true` when the `openapi` option object is present.
17
+ */
18
+ enabled?: boolean;
19
+ /** API title shown in the docs. Defaults to `'Halifax API'`. */
20
+ title?: string;
21
+ /** API version string shown in the docs. Defaults to `'1.0.0'`. */
22
+ version?: string;
23
+ /** Optional markdown description shown at the top of the docs. */
24
+ description?: string;
25
+ /** Server URLs listed in the spec (e.g. `[{ url: 'https://api.example.com/v1' }]`). */
26
+ servers?: Array<{
27
+ url: string;
28
+ description?: string;
29
+ }>;
30
+ /**
31
+ * API-wide response envelope key — mirrors the `envelope` option on `CrudApiOptions`.
32
+ * When set, every success response body is wrapped under this key.
33
+ * Per-resource `ResourceDefinition.envelope` takes precedence.
34
+ */
35
+ envelope?: string | null;
36
+ /**
37
+ * Path for the raw OpenAPI JSON endpoint, relative to the router mount point.
38
+ * Defaults to `'/openapi.json'` → full path is `<mountPoint>/openapi.json`.
39
+ */
40
+ specPath?: string;
41
+ /**
42
+ * Path for the Swagger UI docs page, relative to the router mount point.
43
+ * Defaults to `'/docs'` → full path is `<mountPoint>/docs`.
44
+ */
45
+ docsPath?: string;
46
+ /**
47
+ * OpenAPI security scheme to document. When provided, Halifax adds the scheme to
48
+ * `components/securitySchemes` and applies it globally to all operations.
49
+ *
50
+ * This is auto-populated from the `authStrategy` when it implements `openApiScheme()`.
51
+ * You can override it here if you use a custom strategy or want a different description.
52
+ *
53
+ * @example API key
54
+ * ```ts
55
+ * securityScheme: { type: 'apiKey', in: 'header', name: 'X-Api-Key' }
56
+ * ```
57
+ * @example Bearer JWT
58
+ * ```ts
59
+ * securityScheme: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }
60
+ * ```
61
+ */
62
+ securityScheme?: SecurityScheme;
63
+ /**
64
+ * When `true`, the `/openapi.json` and `/docs` routes require authentication via the
65
+ * configured `authStrategy` — unauthenticated callers receive 401/403 just like any
66
+ * other protected route. Defaults to `false` (docs are publicly accessible).
67
+ */
68
+ requireAuth?: boolean;
69
+ }
70
+ type JsonSchema = {
71
+ type?: string | string[];
72
+ format?: string;
73
+ properties?: Record<string, JsonSchema>;
74
+ additionalProperties?: boolean | JsonSchema;
75
+ items?: JsonSchema;
76
+ required?: string[];
77
+ description?: string;
78
+ nullable?: boolean;
79
+ $ref?: string;
80
+ enum?: unknown[];
81
+ oneOf?: JsonSchema[];
82
+ anyOf?: JsonSchema[];
83
+ allOf?: JsonSchema[];
84
+ minimum?: number;
85
+ default?: unknown;
86
+ example?: unknown;
87
+ readOnly?: boolean;
88
+ };
89
+ type OpenApiParameter = {
90
+ name: string;
91
+ in: 'query' | 'path' | 'header';
92
+ description?: string;
93
+ required?: boolean;
94
+ schema: JsonSchema;
95
+ };
96
+ type OpenApiOperation = {
97
+ operationId?: string;
98
+ summary?: string;
99
+ description?: string;
100
+ tags?: string[];
101
+ parameters?: OpenApiParameter[];
102
+ requestBody?: {
103
+ required: boolean;
104
+ content: {
105
+ 'application/json': {
106
+ schema: JsonSchema;
107
+ };
108
+ };
109
+ };
110
+ responses: Record<string, {
111
+ description: string;
112
+ content?: {
113
+ 'application/json': {
114
+ schema: JsonSchema;
115
+ };
116
+ };
117
+ }>;
118
+ };
119
+ type OpenApiSecuritySchemeObject = {
120
+ type: 'apiKey';
121
+ in: string;
122
+ name: string;
123
+ description?: string;
124
+ } | {
125
+ type: 'http';
126
+ scheme: string;
127
+ bearerFormat?: string;
128
+ description?: string;
129
+ };
130
+ type OpenApiSpec = {
131
+ openapi: '3.1.0';
132
+ info: {
133
+ title: string;
134
+ version: string;
135
+ description?: string;
136
+ };
137
+ servers?: Array<{
138
+ url: string;
139
+ description?: string;
140
+ }>;
141
+ security?: Array<Record<string, []>>;
142
+ paths: Record<string, Partial<Record<'get' | 'post' | 'put' | 'patch' | 'delete', OpenApiOperation>>>;
143
+ components: {
144
+ schemas: Record<string, JsonSchema>;
145
+ securitySchemes?: Record<string, OpenApiSecuritySchemeObject>;
146
+ };
147
+ };
148
+ export declare function generateOpenApiSpec(resources: ResourceDefinition[], options?: OpenApiOptions): OpenApiSpec;
149
+ export {};