@tahminator/sapling 2.0.3 → 2.0.5
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 +94 -4
- package/dist/index.cjs +92 -18
- package/dist/index.d.cts +74 -0
- package/dist/index.d.mts +74 -0
- package/dist/index.mjs +92 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -200,12 +200,71 @@ class UserController {
|
|
|
200
200
|
}
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
Sapling ships with default error middlewares, and you can also write your own.
|
|
204
|
+
Register error middlewares after your regular middlewares and controllers:
|
|
204
205
|
|
|
205
206
|
```typescript
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
import {
|
|
208
|
+
DefaultBaseErrorMiddleware,
|
|
209
|
+
DefaultResponseStatusErrorMiddleware,
|
|
210
|
+
} from "@tahminator/sapling";
|
|
211
|
+
|
|
212
|
+
// regular middlewares & controllers first
|
|
213
|
+
const middlewares: Class<any>[] = [CookieParserMiddleware];
|
|
214
|
+
middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
215
|
+
|
|
216
|
+
const controllers: Class<any>[] = [UserController];
|
|
217
|
+
controllers.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
218
|
+
|
|
219
|
+
// error middlewares last
|
|
220
|
+
const errorMiddlewares: Class<any>[] = [
|
|
221
|
+
DefaultResponseStatusErrorMiddleware,
|
|
222
|
+
DefaultBaseErrorMiddleware,
|
|
223
|
+
];
|
|
224
|
+
errorMiddlewares.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
You can also write your own error middlewares. A specific handler should call
|
|
228
|
+
`next(err)` when it does not handle the error, and a base handler should be last
|
|
229
|
+
and return a response:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
@MiddlewareClass()
|
|
233
|
+
class ResponseStatusErrorMiddleware {
|
|
234
|
+
@Middleware()
|
|
235
|
+
handle(
|
|
236
|
+
err: unknown,
|
|
237
|
+
_request: Request,
|
|
238
|
+
_response: Response,
|
|
239
|
+
next: NextFunction,
|
|
240
|
+
) {
|
|
241
|
+
if (err instanceof ResponseStatusError) {
|
|
242
|
+
return ResponseEntity.status(err.status).body({ message: err.message });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// MUST call next(err) to continue the chain
|
|
246
|
+
next(err);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@MiddlewareClass()
|
|
251
|
+
class BaseErrorMiddleware {
|
|
252
|
+
@Middleware()
|
|
253
|
+
handle(
|
|
254
|
+
err: unknown,
|
|
255
|
+
_request: Request,
|
|
256
|
+
_response: Response,
|
|
257
|
+
_next: NextFunction,
|
|
258
|
+
) {
|
|
259
|
+
console.error("[Error]", err);
|
|
260
|
+
|
|
261
|
+
return ResponseEntity.status(500).body({
|
|
262
|
+
message: "Internal Server Error",
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// no next(err) since last middleware in chain, we are done propagating
|
|
266
|
+
}
|
|
267
|
+
}
|
|
209
268
|
```
|
|
210
269
|
|
|
211
270
|
### Middleware
|
|
@@ -234,10 +293,41 @@ class CookieParserMiddleware {
|
|
|
234
293
|
// Register it like any controller
|
|
235
294
|
app.use(Sapling.resolve(CookieParserMiddleware));
|
|
236
295
|
|
|
296
|
+
// Register middlewares before controllers
|
|
297
|
+
app.use(Sapling.resolve(UserController));
|
|
298
|
+
|
|
237
299
|
// You can also still choose to load plugins the Express.js way
|
|
238
300
|
app.use(cookieParser());
|
|
239
301
|
```
|
|
240
302
|
|
|
303
|
+
You can also write custom middlewares as well. It is functionally the same way as Express: call `next()` explicitly to
|
|
304
|
+
continue down the chain:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { MiddlewareClass, Middleware } from "@tahminator/sapling";
|
|
308
|
+
import { NextFunction, Request, Response } from "express";
|
|
309
|
+
|
|
310
|
+
@MiddlewareClass()
|
|
311
|
+
class RequestTimerMiddleware {
|
|
312
|
+
@Middleware()
|
|
313
|
+
handle(request: Request, _response: Response, next: NextFunction) {
|
|
314
|
+
const start = Date.now();
|
|
315
|
+
|
|
316
|
+
request.on("finish", () => {
|
|
317
|
+
const elapsedMs = Date.now() - start;
|
|
318
|
+
console.log(`[Request] ${request.method} ${request.path} ${elapsedMs}ms`);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// MUST call next() to continue the chain
|
|
322
|
+
next();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Register middlewares before controllers
|
|
327
|
+
app.use(Sapling.resolve(RequestTimerMiddleware));
|
|
328
|
+
app.use(Sapling.resolve(UserController));
|
|
329
|
+
```
|
|
330
|
+
|
|
241
331
|
### Request Validation
|
|
242
332
|
|
|
243
333
|
Validate and transform request bodies, route params, and query strings at the controller level using `@RequestBody`, `@RequestParam`, and `@RequestQuery`. These decorators accept any [Standard Schema](https://github.com/standard-schema/standard-schema) compatible validator (Zod, Valibot, ArkType, etc.).
|
package/dist/index.cjs
CHANGED
|
@@ -505,24 +505,31 @@ function _resolve(ctor) {
|
|
|
505
505
|
//#endregion
|
|
506
506
|
//#region src/annotation/request.ts
|
|
507
507
|
const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
508
|
+
/**
|
|
509
|
+
* Apply to a route method to have `request.body` be parsed by `schema`.
|
|
510
|
+
*
|
|
511
|
+
* This annotation will parse `request.body` & then override `request.body`.
|
|
512
|
+
* You can then just simply cast `request.body` for your use
|
|
513
|
+
*
|
|
514
|
+
* @example
|
|
515
|
+
* ```ts
|
|
516
|
+
* const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
|
|
517
|
+
* name: z.string(),
|
|
518
|
+
* description: z.string().optional(),
|
|
519
|
+
* });
|
|
520
|
+
*
|
|
521
|
+
* ⠀@Controller({ prefix: "/api/book" })
|
|
522
|
+
* class BookController {
|
|
523
|
+
* ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
|
|
524
|
+
* ⠀@POST()
|
|
525
|
+
* public createBook(request: e.Request) {
|
|
526
|
+
* const { name, description } = request.body as unknown as z.infer<
|
|
527
|
+
* typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
|
|
528
|
+
* >;
|
|
529
|
+
* }
|
|
530
|
+
* }
|
|
531
|
+
* ```
|
|
532
|
+
*/
|
|
526
533
|
function RequestBody(schema) {
|
|
527
534
|
return (target, propertyKey) => {
|
|
528
535
|
const ctor = target.constructor;
|
|
@@ -530,6 +537,30 @@ function RequestBody(schema) {
|
|
|
530
537
|
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "body", schema, fnName);
|
|
531
538
|
};
|
|
532
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* Apply to a route method to have `request.param` be parsed by `schema`.
|
|
542
|
+
*
|
|
543
|
+
* This annotation will parse `request.param` & then override `request.param`.
|
|
544
|
+
* You can then just simply cast `request.param` for your use
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* ```ts
|
|
548
|
+
* const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
|
|
549
|
+
* bookId: z.string(),
|
|
550
|
+
* });
|
|
551
|
+
*
|
|
552
|
+
* ⠀@Controller({ prefix: "/api/book" })
|
|
553
|
+
* class BookController {
|
|
554
|
+
* ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
|
|
555
|
+
* ⠀@GET("/:bookId")
|
|
556
|
+
* public getBook(request: e.Request) {
|
|
557
|
+
* const { bookId } = request.param as unknown as z.infer<
|
|
558
|
+
* typeof GET_BOOK_REQUEST_PARAM_SCHEMA
|
|
559
|
+
* >;
|
|
560
|
+
* }
|
|
561
|
+
* }
|
|
562
|
+
* ```
|
|
563
|
+
*/
|
|
533
564
|
function RequestParam(schema) {
|
|
534
565
|
return (target, propertyKey) => {
|
|
535
566
|
const ctor = target.constructor;
|
|
@@ -537,6 +568,31 @@ function RequestParam(schema) {
|
|
|
537
568
|
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "param", schema, fnName);
|
|
538
569
|
};
|
|
539
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* Apply to a route method to have `request.query` be parsed by `schema`.
|
|
573
|
+
*
|
|
574
|
+
* This annotation will parse `request.query` & then override `request.query`.
|
|
575
|
+
* You can then just simply cast `request.query` for your use
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* ```ts
|
|
579
|
+
* const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
|
|
580
|
+
* sort: z.enum(["name", "createdAt"]).optional(),
|
|
581
|
+
* q: z.string().optional(),
|
|
582
|
+
* });
|
|
583
|
+
*
|
|
584
|
+
* ⠀@Controller({ prefix: "/api/book" })
|
|
585
|
+
* class BookController {
|
|
586
|
+
* ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
|
|
587
|
+
* ⠀@GET()
|
|
588
|
+
* public listBooks(request: e.Request) {
|
|
589
|
+
* const { sort, q } = request.query as unknown as z.infer<
|
|
590
|
+
* typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
|
|
591
|
+
* >;
|
|
592
|
+
* }
|
|
593
|
+
* }
|
|
594
|
+
* ```
|
|
595
|
+
*/
|
|
540
596
|
function RequestQuery(schema) {
|
|
541
597
|
return (target, propertyKey) => {
|
|
542
598
|
const ctor = target.constructor;
|
|
@@ -544,6 +600,24 @@ function RequestQuery(schema) {
|
|
|
544
600
|
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "query", schema, fnName);
|
|
545
601
|
};
|
|
546
602
|
}
|
|
603
|
+
function _getOrCreateRequestSchemaDefinition(ctor, fnName) {
|
|
604
|
+
const byFn = (() => {
|
|
605
|
+
const fn = _requestSchemaStore.get(ctor);
|
|
606
|
+
if (fn) return fn;
|
|
607
|
+
const newFn = /* @__PURE__ */ new Map();
|
|
608
|
+
_requestSchemaStore.set(ctor, newFn);
|
|
609
|
+
return newFn;
|
|
610
|
+
})();
|
|
611
|
+
const existing = byFn.get(fnName);
|
|
612
|
+
if (existing) return existing;
|
|
613
|
+
const created = {};
|
|
614
|
+
byFn.set(fnName, created);
|
|
615
|
+
return created;
|
|
616
|
+
}
|
|
617
|
+
function _setOnce(def, key, schema, fnName) {
|
|
618
|
+
if (def[key]) throw new Error(`Duplicate request schema for "${String(key)}" on method "${fnName}"`);
|
|
619
|
+
def[key] = schema;
|
|
620
|
+
}
|
|
547
621
|
function _getRequestSchemas(ctor, fnName) {
|
|
548
622
|
return _requestSchemaStore.get(ctor)?.get(fnName);
|
|
549
623
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -489,8 +489,82 @@ type RequestSchemaDefinition = {
|
|
|
489
489
|
param?: StandardSchemaV1;
|
|
490
490
|
query?: StandardSchemaV1;
|
|
491
491
|
};
|
|
492
|
+
/**
|
|
493
|
+
* Apply to a route method to have `request.body` be parsed by `schema`.
|
|
494
|
+
*
|
|
495
|
+
* This annotation will parse `request.body` & then override `request.body`.
|
|
496
|
+
* You can then just simply cast `request.body` for your use
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* ```ts
|
|
500
|
+
* const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
|
|
501
|
+
* name: z.string(),
|
|
502
|
+
* description: z.string().optional(),
|
|
503
|
+
* });
|
|
504
|
+
*
|
|
505
|
+
* ⠀@Controller({ prefix: "/api/book" })
|
|
506
|
+
* class BookController {
|
|
507
|
+
* ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
|
|
508
|
+
* ⠀@POST()
|
|
509
|
+
* public createBook(request: e.Request) {
|
|
510
|
+
* const { name, description } = request.body as unknown as z.infer<
|
|
511
|
+
* typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
|
|
512
|
+
* >;
|
|
513
|
+
* }
|
|
514
|
+
* }
|
|
515
|
+
* ```
|
|
516
|
+
*/
|
|
492
517
|
declare function RequestBody(schema: StandardSchemaV1): MethodDecorator;
|
|
518
|
+
/**
|
|
519
|
+
* Apply to a route method to have `request.param` be parsed by `schema`.
|
|
520
|
+
*
|
|
521
|
+
* This annotation will parse `request.param` & then override `request.param`.
|
|
522
|
+
* You can then just simply cast `request.param` for your use
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* ```ts
|
|
526
|
+
* const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
|
|
527
|
+
* bookId: z.string(),
|
|
528
|
+
* });
|
|
529
|
+
*
|
|
530
|
+
* ⠀@Controller({ prefix: "/api/book" })
|
|
531
|
+
* class BookController {
|
|
532
|
+
* ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
|
|
533
|
+
* ⠀@GET("/:bookId")
|
|
534
|
+
* public getBook(request: e.Request) {
|
|
535
|
+
* const { bookId } = request.param as unknown as z.infer<
|
|
536
|
+
* typeof GET_BOOK_REQUEST_PARAM_SCHEMA
|
|
537
|
+
* >;
|
|
538
|
+
* }
|
|
539
|
+
* }
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
493
542
|
declare function RequestParam(schema: StandardSchemaV1): MethodDecorator;
|
|
543
|
+
/**
|
|
544
|
+
* Apply to a route method to have `request.query` be parsed by `schema`.
|
|
545
|
+
*
|
|
546
|
+
* This annotation will parse `request.query` & then override `request.query`.
|
|
547
|
+
* You can then just simply cast `request.query` for your use
|
|
548
|
+
*
|
|
549
|
+
* @example
|
|
550
|
+
* ```ts
|
|
551
|
+
* const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
|
|
552
|
+
* sort: z.enum(["name", "createdAt"]).optional(),
|
|
553
|
+
* q: z.string().optional(),
|
|
554
|
+
* });
|
|
555
|
+
*
|
|
556
|
+
* ⠀@Controller({ prefix: "/api/book" })
|
|
557
|
+
* class BookController {
|
|
558
|
+
* ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
|
|
559
|
+
* ⠀@GET()
|
|
560
|
+
* public listBooks(request: e.Request) {
|
|
561
|
+
* const { sort, q } = request.query as unknown as z.infer<
|
|
562
|
+
* typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
|
|
563
|
+
* >;
|
|
564
|
+
* }
|
|
565
|
+
* }
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
494
568
|
declare function RequestQuery(schema: StandardSchemaV1): MethodDecorator;
|
|
495
569
|
declare function _getRequestSchemas(ctor: Function, fnName: string): RequestSchemaDefinition | undefined;
|
|
496
570
|
declare function _parseOrThrow<TSchema extends StandardSchemaV1>(schema: TSchema, input: unknown, kind: ParserErrorLocation): Promise<StandardSchemaV1.InferOutput<TSchema>>;
|
package/dist/index.d.mts
CHANGED
|
@@ -489,8 +489,82 @@ type RequestSchemaDefinition = {
|
|
|
489
489
|
param?: StandardSchemaV1;
|
|
490
490
|
query?: StandardSchemaV1;
|
|
491
491
|
};
|
|
492
|
+
/**
|
|
493
|
+
* Apply to a route method to have `request.body` be parsed by `schema`.
|
|
494
|
+
*
|
|
495
|
+
* This annotation will parse `request.body` & then override `request.body`.
|
|
496
|
+
* You can then just simply cast `request.body` for your use
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* ```ts
|
|
500
|
+
* const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
|
|
501
|
+
* name: z.string(),
|
|
502
|
+
* description: z.string().optional(),
|
|
503
|
+
* });
|
|
504
|
+
*
|
|
505
|
+
* ⠀@Controller({ prefix: "/api/book" })
|
|
506
|
+
* class BookController {
|
|
507
|
+
* ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
|
|
508
|
+
* ⠀@POST()
|
|
509
|
+
* public createBook(request: e.Request) {
|
|
510
|
+
* const { name, description } = request.body as unknown as z.infer<
|
|
511
|
+
* typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
|
|
512
|
+
* >;
|
|
513
|
+
* }
|
|
514
|
+
* }
|
|
515
|
+
* ```
|
|
516
|
+
*/
|
|
492
517
|
declare function RequestBody(schema: StandardSchemaV1): MethodDecorator;
|
|
518
|
+
/**
|
|
519
|
+
* Apply to a route method to have `request.param` be parsed by `schema`.
|
|
520
|
+
*
|
|
521
|
+
* This annotation will parse `request.param` & then override `request.param`.
|
|
522
|
+
* You can then just simply cast `request.param` for your use
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* ```ts
|
|
526
|
+
* const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
|
|
527
|
+
* bookId: z.string(),
|
|
528
|
+
* });
|
|
529
|
+
*
|
|
530
|
+
* ⠀@Controller({ prefix: "/api/book" })
|
|
531
|
+
* class BookController {
|
|
532
|
+
* ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
|
|
533
|
+
* ⠀@GET("/:bookId")
|
|
534
|
+
* public getBook(request: e.Request) {
|
|
535
|
+
* const { bookId } = request.param as unknown as z.infer<
|
|
536
|
+
* typeof GET_BOOK_REQUEST_PARAM_SCHEMA
|
|
537
|
+
* >;
|
|
538
|
+
* }
|
|
539
|
+
* }
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
493
542
|
declare function RequestParam(schema: StandardSchemaV1): MethodDecorator;
|
|
543
|
+
/**
|
|
544
|
+
* Apply to a route method to have `request.query` be parsed by `schema`.
|
|
545
|
+
*
|
|
546
|
+
* This annotation will parse `request.query` & then override `request.query`.
|
|
547
|
+
* You can then just simply cast `request.query` for your use
|
|
548
|
+
*
|
|
549
|
+
* @example
|
|
550
|
+
* ```ts
|
|
551
|
+
* const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
|
|
552
|
+
* sort: z.enum(["name", "createdAt"]).optional(),
|
|
553
|
+
* q: z.string().optional(),
|
|
554
|
+
* });
|
|
555
|
+
*
|
|
556
|
+
* ⠀@Controller({ prefix: "/api/book" })
|
|
557
|
+
* class BookController {
|
|
558
|
+
* ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
|
|
559
|
+
* ⠀@GET()
|
|
560
|
+
* public listBooks(request: e.Request) {
|
|
561
|
+
* const { sort, q } = request.query as unknown as z.infer<
|
|
562
|
+
* typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
|
|
563
|
+
* >;
|
|
564
|
+
* }
|
|
565
|
+
* }
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
494
568
|
declare function RequestQuery(schema: StandardSchemaV1): MethodDecorator;
|
|
495
569
|
declare function _getRequestSchemas(ctor: Function, fnName: string): RequestSchemaDefinition | undefined;
|
|
496
570
|
declare function _parseOrThrow<TSchema extends StandardSchemaV1>(schema: TSchema, input: unknown, kind: ParserErrorLocation): Promise<StandardSchemaV1.InferOutput<TSchema>>;
|
package/dist/index.mjs
CHANGED
|
@@ -481,24 +481,31 @@ function _resolve(ctor) {
|
|
|
481
481
|
//#endregion
|
|
482
482
|
//#region src/annotation/request.ts
|
|
483
483
|
const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
484
|
+
/**
|
|
485
|
+
* Apply to a route method to have `request.body` be parsed by `schema`.
|
|
486
|
+
*
|
|
487
|
+
* This annotation will parse `request.body` & then override `request.body`.
|
|
488
|
+
* You can then just simply cast `request.body` for your use
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* ```ts
|
|
492
|
+
* const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
|
|
493
|
+
* name: z.string(),
|
|
494
|
+
* description: z.string().optional(),
|
|
495
|
+
* });
|
|
496
|
+
*
|
|
497
|
+
* ⠀@Controller({ prefix: "/api/book" })
|
|
498
|
+
* class BookController {
|
|
499
|
+
* ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
|
|
500
|
+
* ⠀@POST()
|
|
501
|
+
* public createBook(request: e.Request) {
|
|
502
|
+
* const { name, description } = request.body as unknown as z.infer<
|
|
503
|
+
* typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
|
|
504
|
+
* >;
|
|
505
|
+
* }
|
|
506
|
+
* }
|
|
507
|
+
* ```
|
|
508
|
+
*/
|
|
502
509
|
function RequestBody(schema) {
|
|
503
510
|
return (target, propertyKey) => {
|
|
504
511
|
const ctor = target.constructor;
|
|
@@ -506,6 +513,30 @@ function RequestBody(schema) {
|
|
|
506
513
|
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "body", schema, fnName);
|
|
507
514
|
};
|
|
508
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Apply to a route method to have `request.param` be parsed by `schema`.
|
|
518
|
+
*
|
|
519
|
+
* This annotation will parse `request.param` & then override `request.param`.
|
|
520
|
+
* You can then just simply cast `request.param` for your use
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* ```ts
|
|
524
|
+
* const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
|
|
525
|
+
* bookId: z.string(),
|
|
526
|
+
* });
|
|
527
|
+
*
|
|
528
|
+
* ⠀@Controller({ prefix: "/api/book" })
|
|
529
|
+
* class BookController {
|
|
530
|
+
* ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
|
|
531
|
+
* ⠀@GET("/:bookId")
|
|
532
|
+
* public getBook(request: e.Request) {
|
|
533
|
+
* const { bookId } = request.param as unknown as z.infer<
|
|
534
|
+
* typeof GET_BOOK_REQUEST_PARAM_SCHEMA
|
|
535
|
+
* >;
|
|
536
|
+
* }
|
|
537
|
+
* }
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
509
540
|
function RequestParam(schema) {
|
|
510
541
|
return (target, propertyKey) => {
|
|
511
542
|
const ctor = target.constructor;
|
|
@@ -513,6 +544,31 @@ function RequestParam(schema) {
|
|
|
513
544
|
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "param", schema, fnName);
|
|
514
545
|
};
|
|
515
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* Apply to a route method to have `request.query` be parsed by `schema`.
|
|
549
|
+
*
|
|
550
|
+
* This annotation will parse `request.query` & then override `request.query`.
|
|
551
|
+
* You can then just simply cast `request.query` for your use
|
|
552
|
+
*
|
|
553
|
+
* @example
|
|
554
|
+
* ```ts
|
|
555
|
+
* const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
|
|
556
|
+
* sort: z.enum(["name", "createdAt"]).optional(),
|
|
557
|
+
* q: z.string().optional(),
|
|
558
|
+
* });
|
|
559
|
+
*
|
|
560
|
+
* ⠀@Controller({ prefix: "/api/book" })
|
|
561
|
+
* class BookController {
|
|
562
|
+
* ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
|
|
563
|
+
* ⠀@GET()
|
|
564
|
+
* public listBooks(request: e.Request) {
|
|
565
|
+
* const { sort, q } = request.query as unknown as z.infer<
|
|
566
|
+
* typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
|
|
567
|
+
* >;
|
|
568
|
+
* }
|
|
569
|
+
* }
|
|
570
|
+
* ```
|
|
571
|
+
*/
|
|
516
572
|
function RequestQuery(schema) {
|
|
517
573
|
return (target, propertyKey) => {
|
|
518
574
|
const ctor = target.constructor;
|
|
@@ -520,6 +576,24 @@ function RequestQuery(schema) {
|
|
|
520
576
|
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "query", schema, fnName);
|
|
521
577
|
};
|
|
522
578
|
}
|
|
579
|
+
function _getOrCreateRequestSchemaDefinition(ctor, fnName) {
|
|
580
|
+
const byFn = (() => {
|
|
581
|
+
const fn = _requestSchemaStore.get(ctor);
|
|
582
|
+
if (fn) return fn;
|
|
583
|
+
const newFn = /* @__PURE__ */ new Map();
|
|
584
|
+
_requestSchemaStore.set(ctor, newFn);
|
|
585
|
+
return newFn;
|
|
586
|
+
})();
|
|
587
|
+
const existing = byFn.get(fnName);
|
|
588
|
+
if (existing) return existing;
|
|
589
|
+
const created = {};
|
|
590
|
+
byFn.set(fnName, created);
|
|
591
|
+
return created;
|
|
592
|
+
}
|
|
593
|
+
function _setOnce(def, key, schema, fnName) {
|
|
594
|
+
if (def[key]) throw new Error(`Duplicate request schema for "${String(key)}" on method "${fnName}"`);
|
|
595
|
+
def[key] = schema;
|
|
596
|
+
}
|
|
523
597
|
function _getRequestSchemas(ctor, fnName) {
|
|
524
598
|
return _requestSchemaStore.get(ctor)?.get(fnName);
|
|
525
599
|
}
|