@tahminator/sapling 2.0.5 → 2.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 +161 -12
- package/dist/index.cjs +591 -262
- package/dist/index.d.cts +589 -88
- package/dist/index.d.mts +589 -88
- package/dist/index.mjs +563 -262
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -19,9 +19,13 @@ A lightweight Express.js dependency injection & route abstraction library.
|
|
|
19
19
|
* [Responses](#responses)
|
|
20
20
|
* [Error Handling](#error-handling)
|
|
21
21
|
* [Middleware](#middleware)
|
|
22
|
-
* [Request Validation](#
|
|
22
|
+
* [Request/Response Validation](#requestresponse-validation)
|
|
23
23
|
* [Redirects](#redirects)
|
|
24
24
|
* [Dependency Injection](#dependency-injection)
|
|
25
|
+
* [OpenAPI Support](#openapi-support)
|
|
26
|
+
+ [Automatic Spec Generation](#automatic-spec-generation)
|
|
27
|
+
+ [Adding Documentation with Decorators](#adding-documentation-with-decorators)
|
|
28
|
+
+ [Customizing Paths](#customizing-paths)
|
|
25
29
|
* [Custom Serialization](#custom-serialization)
|
|
26
30
|
- [Advanced Setup](#advanced-setup)
|
|
27
31
|
* [Automatically import controllers](#automatically-import-controllers)
|
|
@@ -107,6 +111,16 @@ middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
|
107
111
|
const controllers: Class<any>[] = [HelloController, UserController];
|
|
108
112
|
controllers.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
109
113
|
|
|
114
|
+
// @MiddlewareClass handling custom errors should be handled after everything else.
|
|
115
|
+
// Sapling includes basic `DefaultResponseStatusErrorMiddleware` & `BaseErrorMiddleware`
|
|
116
|
+
// but you may write your own, even to replace these defaults!
|
|
117
|
+
const errorMiddlewares: Class<any>[] = [
|
|
118
|
+
DefaultParserErrorMiddleware,
|
|
119
|
+
DefaultResponseStatusErrorMiddleware,
|
|
120
|
+
DefaultBaseErrorMiddleware,
|
|
121
|
+
];
|
|
122
|
+
errorMiddlewares.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
123
|
+
|
|
110
124
|
app.listen(3000);
|
|
111
125
|
```
|
|
112
126
|
|
|
@@ -142,12 +156,9 @@ Sapling supports the usual suspects:
|
|
|
142
156
|
- `@PUT(path?)`
|
|
143
157
|
- `@DELETE(path?)`
|
|
144
158
|
- `@PATCH(path?)`
|
|
145
|
-
- `@Middleware(path?)` - for middleware
|
|
146
|
-
- `@RequestBody(schema)` - validate & parse the request body
|
|
147
|
-
- `@RequestParam(schema)` - validate & parse route params
|
|
148
|
-
- `@RequestQuery(schema)` - validate & parse the query string
|
|
159
|
+
- `@Middleware(path?)` (alias of `@USE(path?)`) - for middleware
|
|
149
160
|
|
|
150
|
-
Path defaults to `"/"` if you don't pass one.
|
|
161
|
+
Path defaults to `"/"` if you don't pass one.
|
|
151
162
|
|
|
152
163
|
### Responses
|
|
153
164
|
|
|
@@ -218,6 +229,7 @@ controllers.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
|
218
229
|
|
|
219
230
|
// error middlewares last
|
|
220
231
|
const errorMiddlewares: Class<any>[] = [
|
|
232
|
+
DefaultParserErrorMiddleware,
|
|
221
233
|
DefaultResponseStatusErrorMiddleware,
|
|
222
234
|
DefaultBaseErrorMiddleware,
|
|
223
235
|
];
|
|
@@ -328,11 +340,18 @@ app.use(Sapling.resolve(RequestTimerMiddleware));
|
|
|
328
340
|
app.use(Sapling.resolve(UserController));
|
|
329
341
|
```
|
|
330
342
|
|
|
331
|
-
### Request Validation
|
|
343
|
+
### Request/Response Validation
|
|
344
|
+
|
|
345
|
+
Validate and transform request request & response inputs / outputs.
|
|
346
|
+
|
|
347
|
+
- `@RequestBody(schema)` - validate & parse the request body
|
|
348
|
+
- `@RequestParam(schema)` - validate & parse route params
|
|
349
|
+
- `@RequestQuery(schema)` - validate & parse the query string
|
|
350
|
+
- `@ResponseBody(schema)` - validate & parse the response body
|
|
332
351
|
|
|
333
|
-
|
|
352
|
+
These decorators accept any [Standard Schema](https://github.com/standard-schema/standard-schema) compatible validation library (Zod, Valibot, ArkType, etc.).
|
|
334
353
|
|
|
335
|
-
If validation fails, a `ParserError` is thrown
|
|
354
|
+
If validation fails, a `ParserError` is thrown. You may register `DefaultParserErrorMiddleware` for default error handling or write your own:
|
|
336
355
|
|
|
337
356
|
```typescript
|
|
338
357
|
import { z } from "zod";
|
|
@@ -340,6 +359,7 @@ import { z } from "zod";
|
|
|
340
359
|
const CreateUserSchema = z.object({ name: z.string(), age: z.number() });
|
|
341
360
|
const UserParamsSchema = z.object({ id: z.string() });
|
|
342
361
|
const ListUsersQuerySchema = z.object({ page: z.coerce.number() });
|
|
362
|
+
const ListUserIdsResponseSchema = z.array(z.object({ id: z.int() }));
|
|
343
363
|
|
|
344
364
|
@Controller({ prefix: "/users" })
|
|
345
365
|
class UserController {
|
|
@@ -366,14 +386,15 @@ class UserController {
|
|
|
366
386
|
}
|
|
367
387
|
|
|
368
388
|
@RequestQuery(ListUsersQuerySchema)
|
|
389
|
+
@ResponseBody(ListUserIdsResponseSchema) // since we have this annotation, it will a) strip all extra fields & b) throw if we are missing any expected fields.
|
|
369
390
|
@GET()
|
|
370
|
-
|
|
391
|
+
listUserIds(request: Request): ResponseEntity<z.infer<typeof ListUserIdsResponseSchema>> {
|
|
371
392
|
// request.query has been fully validated and rewritten. you can safely assert the type!
|
|
372
393
|
const query = request.query as unknown as z.infer<typeof ListUsersQuerySchema>;
|
|
373
394
|
|
|
374
|
-
const
|
|
395
|
+
const userIds = this.database.user.findAll({ page: query.page }).map(u => u.id);
|
|
375
396
|
|
|
376
|
-
return ResponseEntity.ok().body(
|
|
397
|
+
return ResponseEntity.ok().body(userIds);
|
|
377
398
|
}
|
|
378
399
|
}
|
|
379
400
|
```
|
|
@@ -432,6 +453,134 @@ class UserRepository {
|
|
|
432
453
|
}
|
|
433
454
|
```
|
|
434
455
|
|
|
456
|
+
### OpenAPI Support
|
|
457
|
+
|
|
458
|
+
Sapling automatically generates OpenAPI 3.0 specifications from your controllers and serves them with Swagger UI.
|
|
459
|
+
|
|
460
|
+
#### Automatic Spec Generation
|
|
461
|
+
|
|
462
|
+
Configure your API metadata and serve the OpenAPI spec:
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
import {
|
|
466
|
+
Sapling,
|
|
467
|
+
DefaultOpenApiMiddleware,
|
|
468
|
+
DefaultSwaggerMiddleware,
|
|
469
|
+
} from "@tahminator/sapling";
|
|
470
|
+
|
|
471
|
+
// Configure API metadata
|
|
472
|
+
Sapling.Extras.swaggerAndOpenApi.setMetadata({
|
|
473
|
+
title: "My API",
|
|
474
|
+
version: "1.0.0",
|
|
475
|
+
description: "API documentation for my application",
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Serve OpenAPI spec at /openapi.json and Swagger UI at /swagger.html
|
|
479
|
+
// modify default routes with `Sapling.Extras.swaggerAndOpenApi`
|
|
480
|
+
const middlewares = [
|
|
481
|
+
DefaultOpenApiMiddleware,
|
|
482
|
+
DefaultSwaggerMiddleware.Serve,
|
|
483
|
+
DefaultSwaggerMiddleware.Setup,
|
|
484
|
+
];
|
|
485
|
+
middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
486
|
+
|
|
487
|
+
// Register your controllers after the OpenAPI middlewares
|
|
488
|
+
const controllers = [BaseController, UserController];
|
|
489
|
+
controllers.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
The OpenAPI spec is automatically generated from your `@RequestBody`, `@RequestParam`, `@RequestQuery`, and `@ResponseBody` decorators.
|
|
493
|
+
|
|
494
|
+
#### Adding Documentation with Decorators
|
|
495
|
+
|
|
496
|
+
Use `@ControllerSchema` and `@RouteSchema` to add rich documentation:
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
import {
|
|
500
|
+
Controller,
|
|
501
|
+
ControllerSchema,
|
|
502
|
+
RouteSchema,
|
|
503
|
+
GET,
|
|
504
|
+
POST,
|
|
505
|
+
RequestBody,
|
|
506
|
+
ResponseBody,
|
|
507
|
+
HttpStatus,
|
|
508
|
+
} from "@tahminator/sapling";
|
|
509
|
+
import { z } from "zod";
|
|
510
|
+
|
|
511
|
+
const UserSchema = z.object({
|
|
512
|
+
id: z.string().describe("User ID"),
|
|
513
|
+
name: z.string().describe("User's full name"),
|
|
514
|
+
email: z.string().email().describe("User's email address"),
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const CreateUserSchema = z.object({
|
|
518
|
+
name: z.string().min(1).describe("User's full name"),
|
|
519
|
+
email: z.string().email().describe("User's email address"),
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const ErrorSchema = z.object({
|
|
523
|
+
message: z.string().describe("Error message"),
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
@ControllerSchema({
|
|
527
|
+
title: "Users",
|
|
528
|
+
description: "User management endpoints",
|
|
529
|
+
})
|
|
530
|
+
@Controller({ prefix: "/users" })
|
|
531
|
+
class UserController {
|
|
532
|
+
@RouteSchema({
|
|
533
|
+
summary: "List all users",
|
|
534
|
+
description: "Returns a paginated list of all users in the system",
|
|
535
|
+
responses: [
|
|
536
|
+
{
|
|
537
|
+
statusCode: HttpStatus.OK,
|
|
538
|
+
description: "List of users",
|
|
539
|
+
schema: z.array(UserSchema),
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
})
|
|
543
|
+
@ResponseBody(z.array(UserSchema))
|
|
544
|
+
@GET()
|
|
545
|
+
listUsers(): ResponseEntity<z.infer<typeof UserSchema>[]> {
|
|
546
|
+
return ResponseEntity.ok().body([...]);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
@RouteSchema({
|
|
550
|
+
summary: "Create a new user",
|
|
551
|
+
description: "Creates a new user with the provided information",
|
|
552
|
+
responses: [
|
|
553
|
+
{
|
|
554
|
+
statusCode: HttpStatus.CREATED,
|
|
555
|
+
description: "User created successfully",
|
|
556
|
+
schema: UserSchema,
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
statusCode: HttpStatus.BAD_REQUEST,
|
|
560
|
+
description: "Invalid request data",
|
|
561
|
+
schema: ErrorSchema,
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
})
|
|
565
|
+
@RequestBody(CreateUserSchema)
|
|
566
|
+
@ResponseBody(UserSchema)
|
|
567
|
+
@POST()
|
|
568
|
+
createUser(request: Request): ResponseEntity<z.infer<typeof UserSchema>> {
|
|
569
|
+
const body = request.body as z.infer<typeof CreateUserSchema>;
|
|
570
|
+
return ResponseEntity.status(HttpStatus.CREATED).body({...});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
#### Customizing Paths
|
|
576
|
+
|
|
577
|
+
By default, OpenAPI spec is served at `/openapi.json` and Swagger UI at `/swagger.html`. Customize these paths:
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
Sapling.Extras.swaggerAndOpenApi.setOpenApiPath("/api-spec.json");
|
|
581
|
+
Sapling.Extras.swaggerAndOpenApi.setSwaggerPath("/api-docs");
|
|
582
|
+
```
|
|
583
|
+
|
|
435
584
|
### Custom Serialization
|
|
436
585
|
|
|
437
586
|
By default, Sapling uses `JSON.stringify` and `JSON.parse` for serialization. You can override these with custom serializers like [superjson](https://github.com/flightcontrolhq/superjson#readme) to automatically handle Dates, BigInts, and more:
|