@kontract/adonis 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.
Files changed (46) hide show
  1. package/README.md +507 -0
  2. package/dist/adapters/controller-registrar.d.ts +313 -0
  3. package/dist/adapters/controller-registrar.d.ts.map +1 -0
  4. package/dist/adapters/controller-registrar.js +324 -0
  5. package/dist/adapters/controller-registrar.js.map +1 -0
  6. package/dist/adapters/index.d.ts +2 -0
  7. package/dist/adapters/index.d.ts.map +1 -0
  8. package/dist/adapters/index.js +2 -0
  9. package/dist/adapters/index.js.map +1 -0
  10. package/dist/adapters/route-registrar.d.ts +53 -0
  11. package/dist/adapters/route-registrar.d.ts.map +1 -0
  12. package/dist/adapters/route-registrar.js +139 -0
  13. package/dist/adapters/route-registrar.js.map +1 -0
  14. package/dist/adapters/router.d.ts +37 -0
  15. package/dist/adapters/router.d.ts.map +1 -0
  16. package/dist/adapters/router.js +129 -0
  17. package/dist/adapters/router.js.map +1 -0
  18. package/dist/builder/index.d.ts +2 -0
  19. package/dist/builder/index.d.ts.map +1 -0
  20. package/dist/builder/index.js +3 -0
  21. package/dist/builder/index.js.map +1 -0
  22. package/dist/builder/openapi-builder.d.ts +100 -0
  23. package/dist/builder/openapi-builder.d.ts.map +1 -0
  24. package/dist/builder/openapi-builder.js +388 -0
  25. package/dist/builder/openapi-builder.js.map +1 -0
  26. package/dist/index.d.ts +34 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +45 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/serializers/index.d.ts +2 -0
  31. package/dist/serializers/index.d.ts.map +1 -0
  32. package/dist/serializers/index.js +2 -0
  33. package/dist/serializers/index.js.map +1 -0
  34. package/dist/serializers/lucid.d.ts +100 -0
  35. package/dist/serializers/lucid.d.ts.map +1 -0
  36. package/dist/serializers/lucid.js +114 -0
  37. package/dist/serializers/lucid.js.map +1 -0
  38. package/dist/validation/ajv.d.ts +42 -0
  39. package/dist/validation/ajv.d.ts.map +1 -0
  40. package/dist/validation/ajv.js +170 -0
  41. package/dist/validation/ajv.js.map +1 -0
  42. package/dist/validation/index.d.ts +2 -0
  43. package/dist/validation/index.d.ts.map +1 -0
  44. package/dist/validation/index.js +3 -0
  45. package/dist/validation/index.js.map +1 -0
  46. package/package.json +96 -0
package/README.md ADDED
@@ -0,0 +1,507 @@
1
+ # @kontract/adonis
2
+
3
+ AdonisJS adapter for [kontract](../kontract). Provides route registration, AJV validation, Lucid ORM serializers, and OpenAPI spec generation for AdonisJS v6.
4
+
5
+ ## Features
6
+
7
+ - **Automatic route registration** - Register decorated endpoints with AdonisJS router
8
+ - **AJV validation** - TypeBox schema validation with type coercion and format support
9
+ - **Lucid serializers** - Automatic serialization of Lucid models and paginators
10
+ - **OpenAPI generation** - Build OpenAPI 3.0/3.1 specs from decorators
11
+ - **Full TypeBox support** - Validate requests against TypeBox schemas
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @kontract/adonis @sinclair/typebox ajv ajv-formats
17
+ ```
18
+
19
+ > **Note:** This package depends on `kontract` (the core library), which will be installed automatically. You can import from either package - the adapter re-exports all commonly used items from the core.
20
+
21
+ ## Quick Start
22
+
23
+ ### 1. Define Your Controller
24
+
25
+ ```typescript
26
+ // app/controllers/books_controller.ts
27
+ import { Api, Endpoint, ok, apiError } from '@kontract/adonis'
28
+ import { Type, Static } from '@sinclair/typebox'
29
+ import Book from '#models/book'
30
+
31
+ const BookSchema = Type.Object({
32
+ id: Type.String({ format: 'uuid' }),
33
+ title: Type.String(),
34
+ author: Type.String(),
35
+ }, { $id: 'Book' })
36
+
37
+ const CreateBookRequest = Type.Object({
38
+ title: Type.String({ minLength: 1 }),
39
+ author: Type.String({ minLength: 1 }),
40
+ }, { $id: 'CreateBookRequest' })
41
+
42
+ @Api({ tag: 'Books', description: 'Book management' })
43
+ export default class BooksController {
44
+ @Endpoint('GET /api/v1/books', {
45
+ summary: 'List all books',
46
+ responses: {
47
+ 200: { schema: Type.Array(BookSchema) },
48
+ },
49
+ })
50
+ async index() {
51
+ const books = await Book.all()
52
+ return ok(Type.Array(BookSchema), books.map(b => b.toResponse()))
53
+ }
54
+
55
+ @Endpoint('POST /api/v1/books', {
56
+ summary: 'Create a book',
57
+ auth: 'required',
58
+ body: CreateBookRequest,
59
+ responses: {
60
+ 201: { schema: BookSchema },
61
+ 422: null,
62
+ },
63
+ })
64
+ async store(
65
+ ctx: HttpContext,
66
+ body: Static<typeof CreateBookRequest>
67
+ ) {
68
+ const book = await Book.create(body)
69
+ return ok(BookSchema, book.toResponse())
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### 2. Register Routes
75
+
76
+ ```typescript
77
+ // start/routes.ts
78
+ import router from '@adonisjs/core/services/router'
79
+ import { registerDecoratorRoutes, validate } from '@kontract/adonis'
80
+
81
+ // Import controllers to trigger decorator registration
82
+ import '#controllers/books_controller'
83
+ import '#controllers/users_controller'
84
+
85
+ // Register all decorated routes
86
+ registerDecoratorRoutes(router, { validate })
87
+ ```
88
+
89
+ ### 3. Generate OpenAPI Spec
90
+
91
+ ```typescript
92
+ // commands/generate_openapi.ts
93
+ import { OpenApiBuilder } from '@kontract/adonis'
94
+
95
+ // Import controllers
96
+ import '#controllers/books_controller'
97
+ import '#controllers/users_controller'
98
+
99
+ const builder = new OpenApiBuilder({
100
+ title: 'My API',
101
+ description: 'API documentation',
102
+ version: '1.0.0',
103
+ servers: [
104
+ { url: 'http://localhost:3333', description: 'Development' },
105
+ ],
106
+ })
107
+
108
+ const spec = builder.build()
109
+ console.log(JSON.stringify(spec, null, 2))
110
+ ```
111
+
112
+ ## Runtime Validation
113
+
114
+ The packages support runtime validation for both **requests** and **responses**.
115
+
116
+ ### Request Validation (Automatic)
117
+
118
+ Request validation happens automatically when you use `registerDecoratorRoutes()`. The route registrar validates `body`, `query`, and `params` against the TypeBox schemas defined in your `@Endpoint` decorators.
119
+
120
+ ```typescript
121
+ @Endpoint('POST /api/v1/books', {
122
+ body: CreateBookRequest, // Validated at runtime
123
+ query: PaginationQuery, // Validated at runtime
124
+ params: BookIdParams, // Validated at runtime
125
+ responses: { 201: BookSchema },
126
+ })
127
+ async store(ctx, body, query, params) {
128
+ // body, query, params are already validated and typed
129
+ }
130
+ ```
131
+
132
+ If validation fails, an `AjvValidationError` is thrown with status 422.
133
+
134
+ ### Response Validation (Optional)
135
+
136
+ Response validation catches contract violations during development. Enable it by calling `defineConfig()` at application startup:
137
+
138
+ ```typescript
139
+ // start/kernel.ts or providers/app_provider.ts
140
+ import { defineConfig } from 'kontract'
141
+ import { createAjvValidator } from '@kontract/adonis'
142
+
143
+ const validator = createAjvValidator()
144
+
145
+ defineConfig({
146
+ openapi: {
147
+ info: { title: 'My API', version: '1.0.0' },
148
+ },
149
+ runtime: {
150
+ validateResponses: process.env.NODE_ENV !== 'production',
151
+ },
152
+ validator: (schema, data) => validator.validate(schema, data),
153
+ })
154
+ ```
155
+
156
+ When enabled, response helpers like `ok()`, `created()`, etc. will validate the response data against the schema and throw `ResponseValidationError` if it doesn't match.
157
+
158
+ ```typescript
159
+ // This will throw in development if user doesn't match UserSchema
160
+ return ok(UserSchema, user)
161
+ ```
162
+
163
+ ## Route Registration
164
+
165
+ ### registerDecoratorRoutes(router, options)
166
+
167
+ Registers all routes defined via `@Endpoint` decorators with the AdonisJS router.
168
+
169
+ ```typescript
170
+ import router from '@adonisjs/core/services/router'
171
+ import { registerDecoratorRoutes, validate } from '@kontract/adonis'
172
+
173
+ registerDecoratorRoutes(router, {
174
+ validate, // AJV validation function
175
+ })
176
+ ```
177
+
178
+ The registrar:
179
+ - Creates routes for each `@Endpoint` decorator
180
+ - Validates request body, query, and params against TypeBox schemas
181
+ - Handles authentication based on `auth` option
182
+ - Calls the controller method with validated data
183
+ - Processes API responses (status codes, JSON, binary)
184
+
185
+ ### Controller Method Signature
186
+
187
+ Controller methods receive validated data as separate parameters:
188
+
189
+ ```typescript
190
+ async store(
191
+ ctx: HttpContext, // AdonisJS context
192
+ body: BodyType, // Validated request body
193
+ query: QueryType, // Validated query parameters
194
+ params: ParamsType // Validated path parameters
195
+ ) {
196
+ // body, query, params are already validated
197
+ }
198
+ ```
199
+
200
+ ## Validation
201
+
202
+ ### validate(schema, data)
203
+
204
+ Validates data against a TypeBox schema. Throws `AjvValidationError` on failure.
205
+
206
+ ```typescript
207
+ import { validate } from '@kontract/adonis'
208
+ import { Type } from '@sinclair/typebox'
209
+
210
+ const schema = Type.Object({
211
+ email: Type.String({ format: 'email' }),
212
+ age: Type.Integer({ minimum: 0 }),
213
+ })
214
+
215
+ try {
216
+ const data = validate(schema, { email: 'user@example.com', age: 25 })
217
+ // data is typed and validated
218
+ } catch (error) {
219
+ if (error instanceof AjvValidationError) {
220
+ console.log(error.errors)
221
+ // [{ field: 'email', message: 'must match format "email"' }]
222
+ }
223
+ }
224
+ ```
225
+
226
+ ### createAjvValidator(options?)
227
+
228
+ Create a customized AJV validator instance.
229
+
230
+ ```typescript
231
+ import { createAjvValidator } from '@kontract/adonis'
232
+
233
+ const validator = createAjvValidator({
234
+ coerceTypes: true, // Convert strings to numbers, etc.
235
+ removeAdditional: true, // Strip unknown properties
236
+ useDefaults: true, // Apply default values
237
+ formats: {
238
+ 'custom-format': (value) => /^[A-Z]+$/.test(value),
239
+ },
240
+ ajvOptions: {
241
+ // Additional AJV options
242
+ },
243
+ })
244
+
245
+ // Validate (returns errors array)
246
+ const errors = validator.validate(schema, data)
247
+
248
+ // Validate or throw
249
+ validator.validateOrThrow(schema, data)
250
+
251
+ // Pre-compile for performance
252
+ const compiled = validator.compile(schema)
253
+ compiled.validate(data)
254
+ compiled.validateOrThrow(data)
255
+ ```
256
+
257
+ ### AjvValidationError
258
+
259
+ Error thrown when validation fails.
260
+
261
+ ```typescript
262
+ import { AjvValidationError } from '@kontract/adonis'
263
+
264
+ try {
265
+ validate(schema, data)
266
+ } catch (error) {
267
+ if (error instanceof AjvValidationError) {
268
+ error.status // 422
269
+ error.code // 'E_VALIDATION_ERROR'
270
+ error.errors // [{ field: 'email', message: '...' }]
271
+ }
272
+ }
273
+ ```
274
+
275
+ ## Serializers
276
+
277
+ Built-in serializers for Lucid models and paginators.
278
+
279
+ ### Type Guards
280
+
281
+ ```typescript
282
+ import {
283
+ isLucidModel, // Has serialize() method
284
+ isTypedModel, // Has toResponse() method
285
+ isPaginator, // Lucid paginator object
286
+ hasSerialize, // Generic serialize check
287
+ } from '@kontract/adonis'
288
+
289
+ if (isTypedModel(data)) {
290
+ return data.toResponse()
291
+ }
292
+
293
+ if (isLucidModel(data)) {
294
+ return data.serialize()
295
+ }
296
+
297
+ if (isPaginator(data)) {
298
+ // { data: [...], meta: { total, perPage, currentPage, ... } }
299
+ }
300
+ ```
301
+
302
+ ### Serializer Registry
303
+
304
+ Serializers are ordered by priority (higher = checked first):
305
+
306
+ | Serializer | Priority | Checks |
307
+ |------------|----------|--------|
308
+ | `paginatorSerializer` | 150 | `isPaginator()` |
309
+ | `typedModelSerializer` | 100 | `isTypedModel()` |
310
+ | `lucidModelSerializer` | 50 | `isLucidModel()` |
311
+ | `serializableSerializer` | 25 | `hasSerialize()` |
312
+
313
+ ```typescript
314
+ import { lucidSerializers } from '@kontract/adonis'
315
+
316
+ // All serializers in priority order
317
+ lucidSerializers
318
+ ```
319
+
320
+ ## OpenAPI Builder
321
+
322
+ ### OpenApiBuilder
323
+
324
+ Generates OpenAPI specifications from decorated controllers.
325
+
326
+ ```typescript
327
+ import { OpenApiBuilder } from '@kontract/adonis'
328
+
329
+ const builder = new OpenApiBuilder({
330
+ title: 'My API',
331
+ description: 'API documentation',
332
+ version: '1.0.0',
333
+ servers: [
334
+ { url: 'https://api.example.com', description: 'Production' },
335
+ { url: 'http://localhost:3333', description: 'Development' },
336
+ ],
337
+ openapiVersion: '3.1.0', // or '3.0.3'
338
+ securityScheme: {
339
+ name: 'BearerAuth',
340
+ type: 'http',
341
+ scheme: 'bearer',
342
+ bearerFormat: 'JWT',
343
+ description: 'JWT access token',
344
+ },
345
+ })
346
+
347
+ const spec = builder.build()
348
+ ```
349
+
350
+ ### OpenApiBuilderOptions
351
+
352
+ ```typescript
353
+ interface OpenApiBuilderOptions {
354
+ title: string
355
+ description: string
356
+ version: string
357
+ servers: Array<{ url: string; description: string }>
358
+ openapiVersion?: '3.0.3' | '3.1.0' // default: '3.1.0'
359
+ securityScheme?: {
360
+ name: string
361
+ type: 'http' | 'apiKey' | 'oauth2'
362
+ scheme?: string
363
+ bearerFormat?: string
364
+ description?: string
365
+ }
366
+ }
367
+ ```
368
+
369
+ ### Generated Features
370
+
371
+ The builder automatically:
372
+
373
+ - **Collects tags** from `@Api` decorators
374
+ - **Converts paths** (`:id` to `{id}`)
375
+ - **Generates operationIds** from method names
376
+ - **Adds security** for `auth: 'required'` endpoints
377
+ - **Adds 401 response** for authenticated endpoints
378
+ - **Registers schemas** in components
379
+ - **Handles file uploads** as multipart/form-data
380
+
381
+ ### OperationId Generation
382
+
383
+ For standard CRUD method names, operationIds are auto-generated:
384
+
385
+ | Method | Path | Generated operationId |
386
+ |--------|------|----------------------|
387
+ | `index` | GET /users | `listUsers` |
388
+ | `show` | GET /users/:id | `getUser` |
389
+ | `store` | POST /users | `createUser` |
390
+ | `update` | PUT /users/:id | `updateUser` |
391
+ | `destroy` | DELETE /users/:id | `deleteUser` |
392
+
393
+ Custom method names use the method name as operationId.
394
+
395
+ ## Re-exports
396
+
397
+ For convenience, the adapter re-exports common items from the core package:
398
+
399
+ ```typescript
400
+ import {
401
+ // Decorators
402
+ Api,
403
+ Endpoint,
404
+
405
+ // Response helpers
406
+ ok,
407
+ created,
408
+ accepted,
409
+ noContent,
410
+ badRequest,
411
+ unauthorized,
412
+ forbidden,
413
+ notFound,
414
+ conflict,
415
+ unprocessableEntity,
416
+ tooManyRequests,
417
+ internalServerError,
418
+ serviceUnavailable,
419
+ binary,
420
+ apiError,
421
+
422
+ // Configuration
423
+ defineConfig,
424
+ getConfig,
425
+
426
+ // Types
427
+ type ApiOptions,
428
+ type EndpointOptions,
429
+ type ApiResponse,
430
+ type BinaryResponse,
431
+ type EndpointMetadata,
432
+ type ApiMetadata,
433
+ } from '@kontract/adonis'
434
+ ```
435
+
436
+ ## Utilities
437
+
438
+ ### stripNestedIds(schema)
439
+
440
+ Removes `$id` from nested schemas to prevent AJV conflicts when the same schema is used multiple times.
441
+
442
+ ```typescript
443
+ import { stripNestedIds } from '@kontract/adonis'
444
+
445
+ const cleanedSchema = stripNestedIds(schema)
446
+ ```
447
+
448
+ ### getDefaultValidator()
449
+
450
+ Get or create the singleton AJV validator instance.
451
+
452
+ ```typescript
453
+ import { getDefaultValidator } from '@kontract/adonis'
454
+
455
+ const validator = getDefaultValidator()
456
+ ```
457
+
458
+ ### resetDefaultValidator()
459
+
460
+ Reset the default validator (useful for testing).
461
+
462
+ ```typescript
463
+ import { resetDefaultValidator } from '@kontract/adonis'
464
+
465
+ beforeEach(() => {
466
+ resetDefaultValidator()
467
+ })
468
+ ```
469
+
470
+ ## Integration Example
471
+
472
+ Complete example with AdonisJS:
473
+
474
+ ```typescript
475
+ // start/routes.ts
476
+ import router from '@adonisjs/core/services/router'
477
+ import { registerDecoratorRoutes, validate, OpenApiBuilder } from '@kontract/adonis'
478
+
479
+ // Import all controllers
480
+ const controllers = import.meta.glob('../app/controllers/**/*.ts', { eager: true })
481
+
482
+ // Register decorator-based routes
483
+ registerDecoratorRoutes(router, { validate })
484
+
485
+ // Serve OpenAPI spec
486
+ router.get('/docs/json', async ({ response }) => {
487
+ const builder = new OpenApiBuilder({
488
+ title: 'My API',
489
+ description: 'API documentation',
490
+ version: '1.0.0',
491
+ servers: [{ url: 'http://localhost:3333', description: 'Development' }],
492
+ })
493
+
494
+ return response.json(builder.build())
495
+ })
496
+ ```
497
+
498
+ ## Peer Dependencies
499
+
500
+ - `@adonisjs/core` ^6.0.0
501
+ - `@sinclair/typebox` >=0.32.0
502
+ - `ajv` ^8.0.0 (optional, required for validation)
503
+ - `@adonisjs/lucid` ^21.0.0 (optional, required for Lucid serializers)
504
+
505
+ ## License
506
+
507
+ MIT