kontract 0.1.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/README.md +435 -0
- package/dist/builder/define-controller.d.ts +115 -0
- package/dist/builder/define-controller.d.ts.map +1 -0
- package/dist/builder/define-controller.js +80 -0
- package/dist/builder/define-controller.js.map +1 -0
- package/dist/builder/define-endpoint.d.ts +157 -0
- package/dist/builder/define-endpoint.d.ts.map +1 -0
- package/dist/builder/define-endpoint.js +103 -0
- package/dist/builder/define-endpoint.js.map +1 -0
- package/dist/builder/define-route.d.ts +191 -0
- package/dist/builder/define-route.d.ts.map +1 -0
- package/dist/builder/define-route.js +124 -0
- package/dist/builder/define-route.js.map +1 -0
- package/dist/builder/index.d.ts +5 -0
- package/dist/builder/index.d.ts.map +1 -0
- package/dist/builder/index.js +7 -0
- package/dist/builder/index.js.map +1 -0
- package/dist/builder/openapi-builder.d.ts +120 -0
- package/dist/builder/openapi-builder.d.ts.map +1 -0
- package/dist/builder/openapi-builder.js +349 -0
- package/dist/builder/openapi-builder.js.map +1 -0
- package/dist/builder/path-params.d.ts +129 -0
- package/dist/builder/path-params.d.ts.map +1 -0
- package/dist/builder/path-params.js +85 -0
- package/dist/builder/path-params.js.map +1 -0
- package/dist/builder/types.d.ts +149 -0
- package/dist/builder/types.d.ts.map +1 -0
- package/dist/builder/types.js +6 -0
- package/dist/builder/types.js.map +1 -0
- package/dist/config/defaults.d.ts +10 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +28 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/define-config.d.ts +50 -0
- package/dist/config/define-config.d.ts.map +1 -0
- package/dist/config/define-config.js +80 -0
- package/dist/config/define-config.js.map +1 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +5 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +103 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/decorators/api.d.ts +35 -0
- package/dist/decorators/api.d.ts.map +1 -0
- package/dist/decorators/api.js +34 -0
- package/dist/decorators/api.js.map +1 -0
- package/dist/decorators/controller.d.ts +35 -0
- package/dist/decorators/controller.d.ts.map +1 -0
- package/dist/decorators/controller.js +34 -0
- package/dist/decorators/controller.js.map +1 -0
- package/dist/decorators/endpoint.d.ts +93 -0
- package/dist/decorators/endpoint.d.ts.map +1 -0
- package/dist/decorators/endpoint.js +108 -0
- package/dist/decorators/endpoint.js.map +1 -0
- package/dist/decorators/index.d.ts +5 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +6 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/route.d.ts +93 -0
- package/dist/decorators/route.d.ts.map +1 -0
- package/dist/decorators/route.js +108 -0
- package/dist/decorators/route.js.map +1 -0
- package/dist/errors/base.d.ts +8 -0
- package/dist/errors/base.d.ts.map +1 -0
- package/dist/errors/base.js +13 -0
- package/dist/errors/base.js.map +1 -0
- package/dist/errors/configuration.d.ts +22 -0
- package/dist/errors/configuration.d.ts.map +1 -0
- package/dist/errors/configuration.js +33 -0
- package/dist/errors/configuration.js.map +1 -0
- package/dist/errors/index.d.ts +4 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +4 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/validation.d.ts +46 -0
- package/dist/errors/validation.d.ts.map +1 -0
- package/dist/errors/validation.js +52 -0
- package/dist/errors/validation.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/metadata/index.d.ts +2 -0
- package/dist/metadata/index.d.ts.map +1 -0
- package/dist/metadata/index.js +2 -0
- package/dist/metadata/index.js.map +1 -0
- package/dist/metadata/storage.d.ts +50 -0
- package/dist/metadata/storage.d.ts.map +1 -0
- package/dist/metadata/storage.js +100 -0
- package/dist/metadata/storage.js.map +1 -0
- package/dist/metadata/types.d.ts +142 -0
- package/dist/metadata/types.d.ts.map +1 -0
- package/dist/metadata/types.js +2 -0
- package/dist/metadata/types.js.map +1 -0
- package/dist/response/helpers.d.ts +132 -0
- package/dist/response/helpers.d.ts.map +1 -0
- package/dist/response/helpers.js +197 -0
- package/dist/response/helpers.js.map +1 -0
- package/dist/response/index.d.ts +4 -0
- package/dist/response/index.d.ts.map +1 -0
- package/dist/response/index.js +4 -0
- package/dist/response/index.js.map +1 -0
- package/dist/response/types.d.ts +59 -0
- package/dist/response/types.d.ts.map +1 -0
- package/dist/response/types.js +26 -0
- package/dist/response/types.js.map +1 -0
- package/dist/runtime/adapter-types.d.ts +119 -0
- package/dist/runtime/adapter-types.d.ts.map +1 -0
- package/dist/runtime/adapter-types.js +2 -0
- package/dist/runtime/adapter-types.js.map +1 -0
- package/dist/runtime/index.d.ts +12 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/response-helpers.d.ts +138 -0
- package/dist/runtime/response-helpers.d.ts.map +1 -0
- package/dist/runtime/response-helpers.js +105 -0
- package/dist/runtime/response-helpers.js.map +1 -0
- package/dist/runtime/route-utils.d.ts +22 -0
- package/dist/runtime/route-utils.d.ts.map +1 -0
- package/dist/runtime/route-utils.js +47 -0
- package/dist/runtime/route-utils.js.map +1 -0
- package/dist/runtime/types.d.ts +125 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +3 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/types.d.ts +55 -0
- package/dist/validation/types.d.ts.map +1 -0
- package/dist/validation/types.js +2 -0
- package/dist/validation/types.js.map +1 -0
- package/package.json +93 -0
package/README.md
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# kontract
|
|
2
|
+
|
|
3
|
+
Framework-agnostic OpenAPI decorator system with TypeBox schema support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Unified `@Endpoint` decorator** - Define route, auth, schemas, and responses in one place
|
|
8
|
+
- **Type-safe response helpers** - `ok()`, `created()`, `notFound()` with compile-time validation
|
|
9
|
+
- **Framework-agnostic** - Core library has no framework dependencies
|
|
10
|
+
- **OpenAPI 3.1.0 & 3.0.3** - Generate specs for either version
|
|
11
|
+
- **TypeBox integration** - Native support for TypeBox schemas
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
**For AdonisJS projects**, use the adapter package instead (it includes this package):
|
|
16
|
+
```bash
|
|
17
|
+
npm install @kontract/adonis @sinclair/typebox ajv ajv-formats
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**For other frameworks** or custom integrations:
|
|
21
|
+
```bash
|
|
22
|
+
npm install kontract @sinclair/typebox
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { Api, Endpoint, ok, apiError } from 'kontract'
|
|
29
|
+
import { Type, Static } from '@sinclair/typebox'
|
|
30
|
+
|
|
31
|
+
// 1. Define your schemas
|
|
32
|
+
const User = Type.Object({
|
|
33
|
+
id: Type.Number(),
|
|
34
|
+
name: Type.String(),
|
|
35
|
+
email: Type.String({ format: 'email' }),
|
|
36
|
+
}, { $id: 'User' })
|
|
37
|
+
|
|
38
|
+
type UserType = Static<typeof User>
|
|
39
|
+
|
|
40
|
+
// 2. Decorate your controllers
|
|
41
|
+
@Api({ tag: 'Users', description: 'User management endpoints' })
|
|
42
|
+
class UsersController {
|
|
43
|
+
@Endpoint('GET /api/v1/users/:id', {
|
|
44
|
+
summary: 'Get a user by ID',
|
|
45
|
+
params: Type.Object({ id: Type.String() }),
|
|
46
|
+
responses: {
|
|
47
|
+
200: { schema: User, description: 'The user' },
|
|
48
|
+
404: null,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
async show(ctx: unknown, body: unknown, query: unknown, params: { id: string }) {
|
|
52
|
+
const user = await findUser(params.id)
|
|
53
|
+
if (!user) {
|
|
54
|
+
return apiError.notFound('User not found')
|
|
55
|
+
}
|
|
56
|
+
return ok(User, user)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Decorators
|
|
62
|
+
|
|
63
|
+
### `@Api(options)`
|
|
64
|
+
|
|
65
|
+
Class decorator for controllers. Groups endpoints under an OpenAPI tag.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
interface ApiOptions {
|
|
69
|
+
tag: string // OpenAPI tag for grouping endpoints
|
|
70
|
+
description?: string // Description of the API group
|
|
71
|
+
prefix?: string // Optional path prefix for all endpoints
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Example:**
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
@Api({
|
|
79
|
+
tag: 'Books',
|
|
80
|
+
description: 'Book management endpoints',
|
|
81
|
+
prefix: '/api/v1'
|
|
82
|
+
})
|
|
83
|
+
class BooksController { }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `@Endpoint(route, options)`
|
|
87
|
+
|
|
88
|
+
Method decorator for endpoints. Defines the route, validation schemas, and OpenAPI documentation.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
interface EndpointOptions {
|
|
92
|
+
summary?: string // Short summary for OpenAPI docs
|
|
93
|
+
description?: string // Detailed description
|
|
94
|
+
operationId?: string // Unique operation ID (auto-generated if not provided)
|
|
95
|
+
deprecated?: boolean // Mark endpoint as deprecated
|
|
96
|
+
auth?: 'required' | 'optional' | 'none' // Authentication requirement
|
|
97
|
+
body?: TSchema // Request body schema (TypeBox)
|
|
98
|
+
query?: TSchema // Query parameters schema
|
|
99
|
+
params?: TSchema // Path parameters schema
|
|
100
|
+
file?: FileUploadConfig // File upload configuration
|
|
101
|
+
responses: Record<number, TSchema | null | { schema: TSchema | null; description?: string }>
|
|
102
|
+
middleware?: unknown[] // Framework-specific middleware
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Examples:**
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// GET with path parameters
|
|
110
|
+
@Endpoint('GET /api/v1/books/:id', {
|
|
111
|
+
summary: 'Get a book by ID',
|
|
112
|
+
params: Type.Object({ id: Type.String({ format: 'uuid' }) }),
|
|
113
|
+
responses: {
|
|
114
|
+
200: { schema: Book, description: 'The book' },
|
|
115
|
+
404: null,
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
async show() { }
|
|
119
|
+
|
|
120
|
+
// POST with body and authentication
|
|
121
|
+
@Endpoint('POST /api/v1/books', {
|
|
122
|
+
summary: 'Create a new book',
|
|
123
|
+
auth: 'required',
|
|
124
|
+
body: CreateBookRequest,
|
|
125
|
+
responses: {
|
|
126
|
+
201: { schema: Book, description: 'Book created' },
|
|
127
|
+
422: { schema: ValidationError },
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
async store() { }
|
|
131
|
+
|
|
132
|
+
// GET with query parameters
|
|
133
|
+
@Endpoint('GET /api/v1/books', {
|
|
134
|
+
summary: 'List books',
|
|
135
|
+
query: Type.Object({
|
|
136
|
+
page: Type.Optional(Type.Integer({ minimum: 1, default: 1 })),
|
|
137
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, default: 20 })),
|
|
138
|
+
search: Type.Optional(Type.String()),
|
|
139
|
+
}),
|
|
140
|
+
responses: {
|
|
141
|
+
200: { schema: BookListResponse },
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
async index() { }
|
|
145
|
+
|
|
146
|
+
// DELETE with no response body
|
|
147
|
+
@Endpoint('DELETE /api/v1/books/:id', {
|
|
148
|
+
summary: 'Delete a book',
|
|
149
|
+
auth: 'required',
|
|
150
|
+
params: Type.Object({ id: Type.String() }),
|
|
151
|
+
responses: {
|
|
152
|
+
204: null,
|
|
153
|
+
404: null,
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
async destroy() { }
|
|
157
|
+
|
|
158
|
+
// File upload
|
|
159
|
+
@Endpoint('POST /api/v1/books/:id/cover', {
|
|
160
|
+
summary: 'Upload book cover',
|
|
161
|
+
auth: 'required',
|
|
162
|
+
file: { fieldName: 'cover', multiple: false },
|
|
163
|
+
responses: {
|
|
164
|
+
200: { schema: Book },
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
async uploadCover() { }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Response Helpers
|
|
171
|
+
|
|
172
|
+
Response helpers create typed API responses. They return a structured object with `status` and `data` properties.
|
|
173
|
+
|
|
174
|
+
### Success Responses
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { ok, created, accepted, noContent } from 'kontract'
|
|
178
|
+
|
|
179
|
+
// 200 OK
|
|
180
|
+
return ok(UserSchema, { id: 1, name: 'John' })
|
|
181
|
+
|
|
182
|
+
// 201 Created
|
|
183
|
+
return created(UserSchema, { id: 1, name: 'John' })
|
|
184
|
+
|
|
185
|
+
// 202 Accepted
|
|
186
|
+
return accepted(JobSchema, { jobId: 'abc123' })
|
|
187
|
+
|
|
188
|
+
// 204 No Content
|
|
189
|
+
return noContent()
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Error Responses
|
|
193
|
+
|
|
194
|
+
For full control over error response data:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import {
|
|
198
|
+
badRequest,
|
|
199
|
+
unauthorized,
|
|
200
|
+
forbidden,
|
|
201
|
+
notFound,
|
|
202
|
+
conflict,
|
|
203
|
+
unprocessableEntity,
|
|
204
|
+
tooManyRequests,
|
|
205
|
+
internalServerError,
|
|
206
|
+
badGateway,
|
|
207
|
+
serviceUnavailable
|
|
208
|
+
} from 'kontract'
|
|
209
|
+
|
|
210
|
+
// All error helpers follow the same pattern: (schema, data)
|
|
211
|
+
return notFound(ErrorSchema, { message: 'Book not found' })
|
|
212
|
+
return unauthorized(ErrorSchema, { message: 'Invalid token' })
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Unified `apiError` Helper
|
|
216
|
+
|
|
217
|
+
For common error patterns with sensible defaults. Uses a standard `ApiErrorBody` structure:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { apiError } from 'kontract'
|
|
221
|
+
|
|
222
|
+
// Use defaults
|
|
223
|
+
return apiError.notFound() // "Resource not found"
|
|
224
|
+
return apiError.unauthorized() // "Authentication required"
|
|
225
|
+
return apiError.forbidden() // "Access denied"
|
|
226
|
+
|
|
227
|
+
// Override message
|
|
228
|
+
return apiError.notFound('Book not found')
|
|
229
|
+
return apiError.serviceUnavailable('External API is down')
|
|
230
|
+
|
|
231
|
+
// Validation errors with field details
|
|
232
|
+
return apiError.validation([
|
|
233
|
+
{ field: 'email', message: 'Invalid email format' },
|
|
234
|
+
{ field: 'age', message: 'Must be a positive number' },
|
|
235
|
+
])
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Available methods:
|
|
239
|
+
| Method | Status | Default Message |
|
|
240
|
+
|--------|--------|-----------------|
|
|
241
|
+
| `apiError.badRequest(msg?)` | 400 | "Bad request" |
|
|
242
|
+
| `apiError.unauthorized(msg?)` | 401 | "Authentication required" |
|
|
243
|
+
| `apiError.forbidden(msg?)` | 403 | "Access denied" |
|
|
244
|
+
| `apiError.notFound(msg?)` | 404 | "Resource not found" |
|
|
245
|
+
| `apiError.conflict(msg?)` | 409 | "Resource conflict" |
|
|
246
|
+
| `apiError.validation(errors)` | 422 | "Validation failed" |
|
|
247
|
+
| `apiError.rateLimited(msg?)` | 429 | "Too many requests" |
|
|
248
|
+
| `apiError.internal(msg?)` | 500 | "Internal server error" |
|
|
249
|
+
| `apiError.serviceUnavailable(msg?)` | 503 | "Service unavailable" |
|
|
250
|
+
| `apiError.externalApi(msg?)` | 502 | "External API error" |
|
|
251
|
+
|
|
252
|
+
### Binary Responses
|
|
253
|
+
|
|
254
|
+
For file downloads:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { binary } from 'kontract'
|
|
258
|
+
|
|
259
|
+
// Return a file download
|
|
260
|
+
return binary(200, 'application/pdf', pdfBuffer, 'report.pdf')
|
|
261
|
+
return binary(200, 'image/png', imageBuffer)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Metadata Access
|
|
265
|
+
|
|
266
|
+
Access registered decorator metadata programmatically:
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import {
|
|
270
|
+
getRegisteredControllers,
|
|
271
|
+
getApiMetadata,
|
|
272
|
+
getEndpointMetadata,
|
|
273
|
+
getControllerMetadata,
|
|
274
|
+
getAllControllerMetadata,
|
|
275
|
+
clearRegistry,
|
|
276
|
+
} from 'kontract'
|
|
277
|
+
|
|
278
|
+
// Get all registered controllers
|
|
279
|
+
const controllers = getRegisteredControllers()
|
|
280
|
+
|
|
281
|
+
// Get @Api metadata for a specific controller
|
|
282
|
+
const apiMeta = getApiMetadata(UsersController)
|
|
283
|
+
// { tag: 'Users', description: 'User management' }
|
|
284
|
+
|
|
285
|
+
// Get all @Endpoint metadata for a controller
|
|
286
|
+
const endpoints = getEndpointMetadata(UsersController)
|
|
287
|
+
// EndpointMetadata[]
|
|
288
|
+
|
|
289
|
+
// Get combined metadata
|
|
290
|
+
const metadata = getControllerMetadata(UsersController)
|
|
291
|
+
// { controller, api: ApiMetadata, endpoints: EndpointMetadata[] }
|
|
292
|
+
|
|
293
|
+
// Get all metadata at once
|
|
294
|
+
const allMetadata = getAllControllerMetadata()
|
|
295
|
+
// Array of { controller, api, endpoints }
|
|
296
|
+
|
|
297
|
+
// Clear registry (useful for testing)
|
|
298
|
+
clearRegistry()
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Error Classes
|
|
302
|
+
|
|
303
|
+
### RequestValidationError
|
|
304
|
+
|
|
305
|
+
Thrown when request validation fails:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
import { RequestValidationError } from 'kontract'
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
validate(schema, data)
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (error instanceof RequestValidationError) {
|
|
314
|
+
error.status // 422
|
|
315
|
+
error.code // 'E_VALIDATION_ERROR'
|
|
316
|
+
error.errors // [{ field: 'email', message: '...' }]
|
|
317
|
+
error.source // 'body' | 'query' | 'params'
|
|
318
|
+
error.schema // The TypeBox schema that failed
|
|
319
|
+
error.data // The data that was validated
|
|
320
|
+
|
|
321
|
+
// Get response-ready format
|
|
322
|
+
const response = error.toResponse()
|
|
323
|
+
// { status: 422, code: 'E_VALIDATION_ERROR', message: 'Validation failed', errors: [...] }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### ResponseValidationError
|
|
329
|
+
|
|
330
|
+
Thrown when response validation fails (development mode only):
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { ResponseValidationError } from 'kontract'
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Configuration Errors
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import {
|
|
340
|
+
ConfigurationError, // General configuration issue
|
|
341
|
+
AdapterNotFoundError, // Framework adapter not found
|
|
342
|
+
SerializerNotFoundError // Serializer not found for data type
|
|
343
|
+
} from 'kontract'
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Types
|
|
347
|
+
|
|
348
|
+
### Response Types
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import type {
|
|
352
|
+
ApiResponse, // { status: number, data: T }
|
|
353
|
+
BinaryResponse, // { status: number, binary: true, contentType: string, data: Buffer, filename?: string }
|
|
354
|
+
AnyResponse, // ApiResponse | BinaryResponse
|
|
355
|
+
ApiErrorBody, // { status: number, code: string, message: string, errors?: [...] }
|
|
356
|
+
ErrorCode, // Error code string type
|
|
357
|
+
} from 'kontract'
|
|
358
|
+
|
|
359
|
+
import { ErrorCodes, isBinaryResponse } from 'kontract'
|
|
360
|
+
|
|
361
|
+
// Check if response is binary
|
|
362
|
+
if (isBinaryResponse(result)) {
|
|
363
|
+
// result.contentType, result.data, result.filename
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Metadata Types
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
import type {
|
|
371
|
+
HttpMethod, // 'get' | 'post' | 'put' | 'patch' | 'delete'
|
|
372
|
+
AuthLevel, // 'required' | 'optional' | 'none'
|
|
373
|
+
RouteString, // 'GET /path' format
|
|
374
|
+
ResponseDefinition, // { schema: TSchema | null, description?: string }
|
|
375
|
+
FileUploadConfig, // { fieldName: string, multiple?: boolean }
|
|
376
|
+
ApiMetadata, // @Api decorator metadata
|
|
377
|
+
EndpointMetadata, // @Endpoint decorator metadata
|
|
378
|
+
ControllerMetadata, // Combined controller metadata
|
|
379
|
+
} from 'kontract'
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### OpenAPI Types
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import type {
|
|
386
|
+
OpenApiVersion, // '3.0.3' | '3.1.0'
|
|
387
|
+
OpenApiDocument, // Full OpenAPI specification document
|
|
388
|
+
OpenApiPathItem, // Path item object
|
|
389
|
+
OpenApiOperation, // Operation object
|
|
390
|
+
OpenApiParameter, // Parameter object
|
|
391
|
+
OpenApiRequestBody, // Request body object
|
|
392
|
+
OpenApiResponse, // Response object
|
|
393
|
+
OpenApiMediaType, // Media type object
|
|
394
|
+
OpenApiSchema, // Schema object
|
|
395
|
+
OpenApiSecurityScheme, // Security scheme object
|
|
396
|
+
} from 'kontract'
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Validation Types
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
import type {
|
|
403
|
+
Validator, // Validator interface
|
|
404
|
+
CompiledValidator, // Pre-compiled validator for performance
|
|
405
|
+
ValidatorOptions, // Validator configuration options
|
|
406
|
+
ValidationErrorDetail, // { field: string, message: string }
|
|
407
|
+
} from 'kontract'
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Runtime/Adapter Types
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
import type {
|
|
414
|
+
RequestContext, // Generic request context
|
|
415
|
+
AuthUser, // Authenticated user type
|
|
416
|
+
AuthResult, // Authentication result
|
|
417
|
+
RouteHandler, // Route handler function
|
|
418
|
+
RouterAdapter, // Router adapter interface
|
|
419
|
+
AuthAdapter, // Authentication adapter interface
|
|
420
|
+
ContainerAdapter, // DI container adapter interface
|
|
421
|
+
ResponseAdapter, // Response adapter interface
|
|
422
|
+
LoggerAdapter, // Logger adapter interface
|
|
423
|
+
FrameworkAdapters, // All adapters combined
|
|
424
|
+
} from 'kontract'
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Framework Adapters
|
|
428
|
+
|
|
429
|
+
This is the core library. For framework-specific implementations, see:
|
|
430
|
+
|
|
431
|
+
- **AdonisJS**: [`@kontract/adonis`](../kontract-adonis)
|
|
432
|
+
|
|
433
|
+
## License
|
|
434
|
+
|
|
435
|
+
MIT
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controller builder for grouping routes under an OpenAPI tag.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* const usersController = defineController({
|
|
7
|
+
* tag: 'Users',
|
|
8
|
+
* description: 'User management routes',
|
|
9
|
+
* }, {
|
|
10
|
+
* getUser,
|
|
11
|
+
* createUser,
|
|
12
|
+
* updateUser,
|
|
13
|
+
* deleteUser,
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import type { TSchema } from '@sinclair/typebox';
|
|
18
|
+
import type { RouteDefinition } from './define-route.js';
|
|
19
|
+
/**
|
|
20
|
+
* Configuration for defineController.
|
|
21
|
+
*/
|
|
22
|
+
export interface ControllerConfig {
|
|
23
|
+
/** OpenAPI tag for grouping routes */
|
|
24
|
+
tag: string;
|
|
25
|
+
/** Description of the controller/API group */
|
|
26
|
+
description?: string;
|
|
27
|
+
/** Optional path prefix for all routes */
|
|
28
|
+
prefix?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Any route definition regardless of its generic parameters.
|
|
32
|
+
* Used in controller definitions to allow mixed route types.
|
|
33
|
+
*/
|
|
34
|
+
export type AnyRouteDefinition = RouteDefinition<string, TSchema | undefined, TSchema | undefined, TSchema | undefined>;
|
|
35
|
+
/**
|
|
36
|
+
* A record of route definitions.
|
|
37
|
+
*/
|
|
38
|
+
export type RouteRecord = Record<string, AnyRouteDefinition>;
|
|
39
|
+
/**
|
|
40
|
+
* Controller definition returned by defineController.
|
|
41
|
+
*/
|
|
42
|
+
export interface ControllerDefinition<T extends RouteRecord = RouteRecord> {
|
|
43
|
+
/** Type discriminator */
|
|
44
|
+
readonly __type: 'controller';
|
|
45
|
+
/** Controller configuration (tag, description, prefix) */
|
|
46
|
+
readonly config: ControllerConfig;
|
|
47
|
+
/** Map of route names to definitions */
|
|
48
|
+
readonly routes: T;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Define a controller that groups routes under an OpenAPI tag.
|
|
52
|
+
*
|
|
53
|
+
* Controllers provide organizational structure for your API:
|
|
54
|
+
* - Group related routes together
|
|
55
|
+
* - Apply a common tag for OpenAPI documentation
|
|
56
|
+
* - Optionally apply a path prefix to all routes
|
|
57
|
+
*
|
|
58
|
+
* @param config - Controller configuration
|
|
59
|
+
* @param routes - Record of route definitions
|
|
60
|
+
* @returns ControllerDefinition for registration with framework adapters
|
|
61
|
+
*
|
|
62
|
+
* @example Basic controller
|
|
63
|
+
* ```typescript
|
|
64
|
+
* import { defineController } from 'kontract'
|
|
65
|
+
* import { getUser, createUser, updateUser, deleteUser } from './user-routes.js'
|
|
66
|
+
*
|
|
67
|
+
* export const usersController = defineController({
|
|
68
|
+
* tag: 'Users',
|
|
69
|
+
* description: 'User management routes',
|
|
70
|
+
* }, {
|
|
71
|
+
* getUser,
|
|
72
|
+
* createUser,
|
|
73
|
+
* updateUser,
|
|
74
|
+
* deleteUser,
|
|
75
|
+
* })
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @example With path prefix
|
|
79
|
+
* ```typescript
|
|
80
|
+
* export const adminController = defineController({
|
|
81
|
+
* tag: 'Admin',
|
|
82
|
+
* description: 'Administrative routes',
|
|
83
|
+
* prefix: '/admin',
|
|
84
|
+
* }, {
|
|
85
|
+
* listUsers: defineRoute({ route: 'GET /users', ... }, ...),
|
|
86
|
+
* // Actual path will be /admin/users
|
|
87
|
+
* })
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @example Registration with adapters
|
|
91
|
+
* ```typescript
|
|
92
|
+
* // Fastify
|
|
93
|
+
* import { registerController } from '@kontract/fastify'
|
|
94
|
+
* registerController(app, usersController, options)
|
|
95
|
+
*
|
|
96
|
+
* // Hono
|
|
97
|
+
* import { registerController } from '@kontract/hono'
|
|
98
|
+
* registerController(app, usersController, options)
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare function defineController<T extends RouteRecord>(config: ControllerConfig, routes: T): ControllerDefinition<T>;
|
|
102
|
+
/**
|
|
103
|
+
* Type guard to check if a value is a ControllerDefinition.
|
|
104
|
+
*/
|
|
105
|
+
export declare function isControllerDefinition(value: unknown): value is ControllerDefinition;
|
|
106
|
+
/**
|
|
107
|
+
* Get all routes from a controller with their full paths.
|
|
108
|
+
* Applies the controller's prefix if configured.
|
|
109
|
+
*/
|
|
110
|
+
export declare function getControllerRoutes(controller: ControllerDefinition): Array<{
|
|
111
|
+
name: string;
|
|
112
|
+
route: AnyRouteDefinition;
|
|
113
|
+
fullPath: string;
|
|
114
|
+
}>;
|
|
115
|
+
//# sourceMappingURL=define-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-controller.d.ts","sourceRoot":"","sources":["../../src/builder/define-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAExD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAA;IACX,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,GAAG,SAAS,CAAC,CAAA;AAEvH;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;AAE5D;;GAEG;AACH,MAAM,WAAW,oBAAoB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW;IACvE,yBAAyB;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;IACjC,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,WAAW,EACpD,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,CAAC,GACR,oBAAoB,CAAC,CAAC,CAAC,CAMzB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CAOpF;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,oBAAoB,GAC/B,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,kBAAkB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAQtE"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Define a controller that groups routes under an OpenAPI tag.
|
|
3
|
+
*
|
|
4
|
+
* Controllers provide organizational structure for your API:
|
|
5
|
+
* - Group related routes together
|
|
6
|
+
* - Apply a common tag for OpenAPI documentation
|
|
7
|
+
* - Optionally apply a path prefix to all routes
|
|
8
|
+
*
|
|
9
|
+
* @param config - Controller configuration
|
|
10
|
+
* @param routes - Record of route definitions
|
|
11
|
+
* @returns ControllerDefinition for registration with framework adapters
|
|
12
|
+
*
|
|
13
|
+
* @example Basic controller
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { defineController } from 'kontract'
|
|
16
|
+
* import { getUser, createUser, updateUser, deleteUser } from './user-routes.js'
|
|
17
|
+
*
|
|
18
|
+
* export const usersController = defineController({
|
|
19
|
+
* tag: 'Users',
|
|
20
|
+
* description: 'User management routes',
|
|
21
|
+
* }, {
|
|
22
|
+
* getUser,
|
|
23
|
+
* createUser,
|
|
24
|
+
* updateUser,
|
|
25
|
+
* deleteUser,
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @example With path prefix
|
|
30
|
+
* ```typescript
|
|
31
|
+
* export const adminController = defineController({
|
|
32
|
+
* tag: 'Admin',
|
|
33
|
+
* description: 'Administrative routes',
|
|
34
|
+
* prefix: '/admin',
|
|
35
|
+
* }, {
|
|
36
|
+
* listUsers: defineRoute({ route: 'GET /users', ... }, ...),
|
|
37
|
+
* // Actual path will be /admin/users
|
|
38
|
+
* })
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example Registration with adapters
|
|
42
|
+
* ```typescript
|
|
43
|
+
* // Fastify
|
|
44
|
+
* import { registerController } from '@kontract/fastify'
|
|
45
|
+
* registerController(app, usersController, options)
|
|
46
|
+
*
|
|
47
|
+
* // Hono
|
|
48
|
+
* import { registerController } from '@kontract/hono'
|
|
49
|
+
* registerController(app, usersController, options)
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function defineController(config, routes) {
|
|
53
|
+
return {
|
|
54
|
+
__type: 'controller',
|
|
55
|
+
config,
|
|
56
|
+
routes,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Type guard to check if a value is a ControllerDefinition.
|
|
61
|
+
*/
|
|
62
|
+
export function isControllerDefinition(value) {
|
|
63
|
+
return (typeof value === 'object'
|
|
64
|
+
&& value !== null
|
|
65
|
+
&& '__type' in value
|
|
66
|
+
&& value.__type === 'controller');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get all routes from a controller with their full paths.
|
|
70
|
+
* Applies the controller's prefix if configured.
|
|
71
|
+
*/
|
|
72
|
+
export function getControllerRoutes(controller) {
|
|
73
|
+
const prefix = controller.config.prefix ?? '';
|
|
74
|
+
return Object.entries(controller.routes).map(([name, route]) => ({
|
|
75
|
+
name,
|
|
76
|
+
route,
|
|
77
|
+
fullPath: prefix + route.path,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=define-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-controller.js","sourceRoot":"","sources":["../../src/builder/define-controller.ts"],"names":[],"mappings":"AAsDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAwB,EACxB,MAAS;IAET,OAAO;QACL,MAAM,EAAE,YAAY;QACpB,MAAM;QACN,MAAM;KACP,CAAA;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACnD,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;WACtB,KAAK,KAAK,IAAI;WACd,QAAQ,IAAI,KAAK;WAChB,KAA6B,CAAC,MAAM,KAAK,YAAY,CAC1D,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,UAAgC;IAEhC,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAA;IAE7C,OAAO,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;QAC/D,IAAI;QACJ,KAAK;QACL,QAAQ,EAAE,MAAM,GAAG,KAAK,CAAC,IAAI;KAC9B,CAAC,CAAC,CAAA;AACL,CAAC"}
|