@tahminator/sapling 1.5.28 → 2.0.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.
package/README.md CHANGED
@@ -19,6 +19,7 @@ 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](#request-validation)
22
23
  * [Redirects](#redirects)
23
24
  * [Dependency Injection](#dependency-injection)
24
25
  * [Custom Serialization](#custom-serialization)
@@ -142,8 +143,11 @@ Sapling supports the usual suspects:
142
143
  - `@DELETE(path?)`
143
144
  - `@PATCH(path?)`
144
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
145
149
 
146
- Path defaults to `"/"` if you don't pass one.
150
+ Path defaults to `"/"` if you don't pass one. The request schema decorators accept any [Standard Schema](https://github.com/standard-schema/standard-schema) compatible validator (e.g. Zod, Valibot, ArkType).
147
151
 
148
152
  ### Responses
149
153
 
@@ -234,6 +238,56 @@ app.use(Sapling.resolve(CookieParserMiddleware));
234
238
  app.use(cookieParser());
235
239
  ```
236
240
 
241
+ ### Request Validation
242
+
243
+ 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.).
244
+
245
+ If validation fails, a `ParserError` is thrown, which Express handles as a `400 Bad Request` by default:
246
+
247
+ ```typescript
248
+ import { z } from "zod";
249
+
250
+ const CreateUserSchema = z.object({ name: z.string(), age: z.number() });
251
+ const UserParamsSchema = z.object({ id: z.string() });
252
+ const ListUsersQuerySchema = z.object({ page: z.coerce.number() });
253
+
254
+ @Controller({ prefix: "/users" })
255
+ class UserController {
256
+ @RequestBody(CreateUserSchema)
257
+ @POST()
258
+ createUser(request: Request): ResponseEntity<User> {
259
+ // request.body has been fully validated and rewritten. you can safely assert the type!
260
+ const requestBody = request.body as unknown as z.infer<CreateUserSchema>;
261
+
262
+ const user = this.database.user.create(requestBody.name, requestBody.age)
263
+
264
+ return ResponseEntity.ok().body(user);
265
+ }
266
+
267
+ @RequestParam(UserParamsSchema)
268
+ @GET("/:id")
269
+ getUser(request: Request): ResponseEntity<z.infer<typeof UserParamsSchema>> {
270
+ // request.params has been fully validated and rewritten. you can safely assert the type!
271
+ const params = request.params as unknown as z.infer<typeof UserParamsSchema>;
272
+
273
+ const user = this.database.user.findById(params.id);
274
+
275
+ return ResponseEntity.ok().body(user);
276
+ }
277
+
278
+ @RequestQuery(ListUsersQuerySchema)
279
+ @GET()
280
+ listUsers(request: Request): ResponseEntity<z.infer<typeof ListUsersQuerySchema>> {
281
+ // request.query has been fully validated and rewritten. you can safely assert the type!
282
+ const query = request.query as unknown as z.infer<typeof ListUsersQuerySchema>;
283
+
284
+ const users = this.database.user.findAll({ page: query.page });
285
+
286
+ return ResponseEntity.ok().body(users);
287
+ }
288
+ }
289
+ ```
290
+
237
291
  ### Redirects
238
292
 
239
293
  ```typescript
package/dist/index.cjs CHANGED
@@ -221,6 +221,46 @@ var ResponseEntityBuilder = class {
221
221
  }
222
222
  };
223
223
  //#endregion
224
+ //#region src/helper/error/responsestatus.ts
225
+ /**
226
+ * Ensure that you define a middleware that can handle this error.
227
+ *
228
+ * @see {@link Sapling.loadResponseStatusErrorMiddleware}
229
+ */
230
+ var ResponseStatusError = class ResponseStatusError extends Error {
231
+ constructor(status, message) {
232
+ super(message ?? "Something went wrong.");
233
+ this.status = status;
234
+ Object.setPrototypeOf(this, new.target.prototype);
235
+ this.name = `HttpError(${HttpStatus[status]})`;
236
+ if (Error.captureStackTrace) Error.captureStackTrace(this, ResponseStatusError);
237
+ }
238
+ };
239
+ //#endregion
240
+ //#region src/helper/error/parse.ts
241
+ /**
242
+ * This error should be thrown when some data cannot be parsed by a given schema.
243
+ */
244
+ var ParserError = class ParserError extends ResponseStatusError {
245
+ constructor(location, issues, vendor) {
246
+ super(400, ParserError.formatMessage(location, issues, vendor));
247
+ Object.setPrototypeOf(this, new.target.prototype);
248
+ }
249
+ static formatMessage(location, issues, vendor) {
250
+ const formatted = issues.map((i) => {
251
+ const path = Array.isArray(i.path) ? i.path.map((seg) => typeof seg === "object" && seg ? String(seg.key) : String(seg)).join(".") : "";
252
+ return path ? `${path}: ${i.message}` : i.message;
253
+ }).join("; ");
254
+ return `${vendor} failed to parse ${(() => {
255
+ switch (location) {
256
+ case "reqbody": return "request body";
257
+ case "reqparams": return "request params";
258
+ case "reqquery": return "request query";
259
+ }
260
+ })()}: ${formatted}`;
261
+ }
262
+ };
263
+ //#endregion
224
264
  //#region src/helper/sapling.ts
225
265
  const settings = {
226
266
  serialize: JSON.stringify,
@@ -294,34 +334,6 @@ var Sapling = class Sapling {
294
334
  app.use(Sapling.json());
295
335
  }
296
336
  /**
297
- * Register a middleware that will handle {@link ResponseStatusError}.
298
- *
299
- * This middleware will chain to the next middleware if it does not catch {@link ResponseStatusError}.
300
- * You may still define middleware to handle all other errors in a separate `app.use` call.
301
- *
302
- * @example
303
- * ```ts
304
- * import express from "express";
305
- * import { Sapling } from "@tahminator/sapling";
306
- *
307
- * const app = express();
308
- *
309
- * Sapling.loadResponseStatusErrorMiddleware(app, (err, req, res, next) => {
310
- * // `err` is guaranteed to be of type ResponseStatusError
311
- * res.status(err.status).json({
312
- * success: false,
313
- * message: err.message,
314
- * });
315
- * });
316
- * ```
317
- */
318
- static loadResponseStatusErrorMiddleware(app, fn) {
319
- app.use(((err, req, res, next) => {
320
- if (err instanceof ResponseStatusError) fn(err, req, res, next);
321
- else next(err);
322
- }));
323
- }
324
- /**
325
337
  * Serialize a value into a JSON string.
326
338
  *
327
339
  * This function is used in {@link ResponseEntity} to serialize the `body`.
@@ -359,22 +371,6 @@ var Sapling = class Sapling {
359
371
  }
360
372
  };
361
373
  //#endregion
362
- //#region src/helper/error.ts
363
- /**
364
- * Ensure that you define a middleware that can handle this error.
365
- *
366
- * @see {@link Sapling.loadResponseStatusErrorMiddleware}
367
- */
368
- var ResponseStatusError = class ResponseStatusError extends Error {
369
- constructor(status, message) {
370
- super(message ?? "Something went wrong.");
371
- this.status = status;
372
- Object.setPrototypeOf(this, new.target.prototype);
373
- this.name = `HttpError(${HttpStatus[status]})`;
374
- if (Error.captureStackTrace) Error.captureStackTrace(this, ResponseStatusError);
375
- }
376
- };
377
- //#endregion
378
374
  //#region src/types.ts
379
375
  const methodResolve = {
380
376
  GET: "get",
@@ -507,6 +503,56 @@ function _resolve(ctor) {
507
503
  return _InjectableRegistry.get(ctor);
508
504
  }
509
505
  //#endregion
506
+ //#region src/annotation/request.ts
507
+ const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
508
+ function _getOrCreateRequestSchemaDefinition(ctor, fnName) {
509
+ const byFn = (() => {
510
+ const fn = _requestSchemaStore.get(ctor);
511
+ if (fn) return fn;
512
+ const newFn = /* @__PURE__ */ new Map();
513
+ _requestSchemaStore.set(ctor, newFn);
514
+ return newFn;
515
+ })();
516
+ const existing = byFn.get(fnName);
517
+ if (existing) return existing;
518
+ const created = {};
519
+ byFn.set(fnName, created);
520
+ return created;
521
+ }
522
+ function _setOnce(def, key, schema, fnName) {
523
+ if (def[key]) throw new Error(`Duplicate request schema for "${String(key)}" on method "${fnName}"`);
524
+ def[key] = schema;
525
+ }
526
+ function RequestBody(schema) {
527
+ return (target, propertyKey) => {
528
+ const ctor = target.constructor;
529
+ const fnName = String(propertyKey);
530
+ _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "body", schema, fnName);
531
+ };
532
+ }
533
+ function RequestParam(schema) {
534
+ return (target, propertyKey) => {
535
+ const ctor = target.constructor;
536
+ const fnName = String(propertyKey);
537
+ _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "param", schema, fnName);
538
+ };
539
+ }
540
+ function RequestQuery(schema) {
541
+ return (target, propertyKey) => {
542
+ const ctor = target.constructor;
543
+ const fnName = String(propertyKey);
544
+ _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "query", schema, fnName);
545
+ };
546
+ }
547
+ function _getRequestSchemas(ctor, fnName) {
548
+ return _requestSchemaStore.get(ctor)?.get(fnName);
549
+ }
550
+ async function _parseOrThrow(schema, input, kind) {
551
+ const result = await schema["~standard"].validate(input);
552
+ if (result.issues) throw new ParserError(kind, result.issues, schema["~standard"].vendor);
553
+ return result.value;
554
+ }
555
+ //#endregion
510
556
  //#region src/annotation/route.ts
511
557
  const _routeStore = /* @__PURE__ */ new WeakMap();
512
558
  /**
@@ -604,6 +650,14 @@ function Controller({ prefix = "", deps = [] } = {}) {
604
650
  const usedRoutes = /* @__PURE__ */ new Set();
605
651
  _InjectableDeps.set(targetClass, deps);
606
652
  const controllerInstance = _resolve(targetClass);
653
+ if (routes.reduce((prev, r) => {
654
+ if (r.method !== "USE") return prev;
655
+ const fn = controllerInstance[r.fnName];
656
+ return typeof fn === "function" && fn.length >= 4 ? prev + 1 : prev;
657
+ }, 0) > 1) throw new Error(`Invalid @MiddlewareClass class "${targetClass.name}":
658
+ Multiple 4-arg @Middleware() error handlers were found.
659
+ Express will not enter routers in error mode, so an error-middleware class must expose exactly one error handler.
660
+ Split these into separate @MiddlewareClass classes, or merge the logic into a single method.`);
607
661
  for (const { method, path, fnName } of routes) {
608
662
  const fn = controllerInstance[fnName];
609
663
  if (typeof fn !== "function") continue;
@@ -612,9 +666,34 @@ function Controller({ prefix = "", deps = [] } = {}) {
612
666
  if (method !== "USE" && usedRoutes.has(routeKey)) throw new Error(`Duplicate route [${method}] "${path instanceof RegExp ? path.source : fp}" detected in controller "${target.name}"`);
613
667
  if (method !== "USE") usedRoutes.add(routeKey);
614
668
  const methodName = methodResolve[method];
669
+ if (method === "USE" && fn.length >= 4) {
670
+ const middlewareFn = async (err, request, response, next) => {
671
+ try {
672
+ const result = fn.bind(controllerInstance)(err, request, response, next);
673
+ if (result instanceof ResponseEntity) {
674
+ response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
675
+ return;
676
+ }
677
+ if (result instanceof RedirectView) {
678
+ response.redirect(result.getUrl());
679
+ return;
680
+ }
681
+ } catch (e) {
682
+ console.error(e);
683
+ next(e);
684
+ }
685
+ };
686
+ _ControllerRegistry.set(targetClass, middlewareFn);
687
+ return;
688
+ }
615
689
  router[methodName](fp, async (request, response, next) => {
690
+ const schemas = _getRequestSchemas(target, fnName);
691
+ if (schemas) {
692
+ if (schemas.body) request.body = await _parseOrThrow(schemas.body, request.body, "reqbody");
693
+ if (schemas.param) request.params = await _parseOrThrow(schemas.param, request.params, "reqparams");
694
+ if (schemas.query) request.query = await _parseOrThrow(schemas.query, request.query, "reqquery");
695
+ }
616
696
  const result = await fn.bind(controllerInstance)(request, response, next);
617
- if (method === "USE") return;
618
697
  if (result instanceof ResponseEntity) {
619
698
  response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
620
699
  return;
@@ -623,7 +702,7 @@ function Controller({ prefix = "", deps = [] } = {}) {
623
702
  response.redirect(result.getUrl());
624
703
  return;
625
704
  }
626
- if (!response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${methodName.toUpperCase()} ${path instanceof RegExp ? path.source : fp}`));
705
+ if (method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${methodName.toUpperCase()} ${path instanceof RegExp ? path.source : fp}`));
627
706
  });
628
707
  }
629
708
  _ControllerRegistry.set(targetClass, router);
@@ -642,8 +721,47 @@ function MiddlewareClass(...args) {
642
721
  return Controller(...args);
643
722
  }
644
723
  //#endregion
724
+ //#region \0@oxc-project+runtime@0.127.0/helpers/decorate.js
725
+ function __decorate(decorators, target, key, desc) {
726
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
727
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
728
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
729
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
730
+ }
731
+ //#endregion
732
+ //#region src/middleware/default/base.ts
733
+ let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
734
+ handle(err, _request, _response, _next) {
735
+ console.error("[Error]", err);
736
+ return ResponseEntity.status(500).body({ message: "Internal Server Error" });
737
+ }
738
+ };
739
+ __decorate([Middleware()], DefaultBaseErrorMiddleware.prototype, "handle", null);
740
+ DefaultBaseErrorMiddleware = __decorate([MiddlewareClass()], DefaultBaseErrorMiddleware);
741
+ //#endregion
742
+ //#region src/middleware/default/responsestatus.ts
743
+ let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddleware {
744
+ handle(err, _request, _response, _next) {
745
+ if (err instanceof ResponseStatusError) return ResponseEntity.status(err.status).body({ message: err.message });
746
+ }
747
+ };
748
+ __decorate([Middleware()], DefaultResponseStatusErrorMiddleware.prototype, "handle", null);
749
+ DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultResponseStatusErrorMiddleware);
750
+ //#endregion
645
751
  exports.Controller = Controller;
646
752
  exports.DELETE = DELETE;
753
+ Object.defineProperty(exports, "DefaultBaseErrorMiddleware", {
754
+ enumerable: true,
755
+ get: function() {
756
+ return DefaultBaseErrorMiddleware;
757
+ }
758
+ });
759
+ Object.defineProperty(exports, "DefaultResponseStatusErrorMiddleware", {
760
+ enumerable: true,
761
+ get: function() {
762
+ return DefaultResponseStatusErrorMiddleware;
763
+ }
764
+ });
647
765
  exports.GET = GET;
648
766
  exports.HEAD = HEAD;
649
767
  exports.Html404ErrorPage = Html404ErrorPage;
@@ -655,7 +773,11 @@ exports.OPTIONS = OPTIONS;
655
773
  exports.PATCH = PATCH;
656
774
  exports.POST = POST;
657
775
  exports.PUT = PUT;
776
+ exports.ParserError = ParserError;
658
777
  exports.RedirectView = RedirectView;
778
+ exports.RequestBody = RequestBody;
779
+ exports.RequestParam = RequestParam;
780
+ exports.RequestQuery = RequestQuery;
659
781
  exports.ResponseEntity = ResponseEntity;
660
782
  exports.ResponseEntityBuilder = ResponseEntityBuilder;
661
783
  exports.ResponseStatusError = ResponseStatusError;
@@ -664,6 +786,8 @@ exports._ControllerRegistry = _ControllerRegistry;
664
786
  exports._InjectableDeps = _InjectableDeps;
665
787
  exports._InjectableRegistry = _InjectableRegistry;
666
788
  exports._Route = _Route;
789
+ exports._getRequestSchemas = _getRequestSchemas;
667
790
  exports._getRoutes = _getRoutes;
791
+ exports._parseOrThrow = _parseOrThrow;
668
792
  exports._resolve = _resolve;
669
793
  exports.methodResolve = methodResolve;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import e, { NextFunction, Request, Response, Router } from "express";
1
+ import e, { ErrorRequestHandler, NextFunction, Request, Response, Router } from "express";
2
2
 
3
3
  //#region src/html/404.d.ts
4
4
  /**
@@ -29,7 +29,7 @@ type HttpHeaders = Record<string, string>;
29
29
  type ExpressMiddlewareFn = ($1: Request, $2: Response, $3: NextFunction) => void;
30
30
  //#endregion
31
31
  //#region src/annotation/controller.d.ts
32
- declare const _ControllerRegistry: WeakMap<Function, Router>;
32
+ declare const _ControllerRegistry: WeakMap<Function, Router | ErrorRequestHandler>;
33
33
  type ControllerProps = {
34
34
  /**
35
35
  * Optional URL prefix applied to all routes in the controller. Defaults to "".
@@ -153,6 +153,84 @@ declare function _getRoutes(ctor: Function): readonly RouteDefinition[];
153
153
  */
154
154
  declare function MiddlewareClass(...args: Parameters<typeof Controller>): ClassDecorator;
155
155
  //#endregion
156
+ //#region node_modules/.pnpm/@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec/dist/index.d.ts
157
+ /** The Standard Typed interface. This is a base type extended by other specs. */
158
+ interface StandardTypedV1<Input = unknown, Output = Input> {
159
+ /** The Standard properties. */
160
+ readonly "~standard": StandardTypedV1.Props<Input, Output>;
161
+ }
162
+ declare namespace StandardTypedV1 {
163
+ /** The Standard Typed properties interface. */
164
+ interface Props<Input = unknown, Output = Input> {
165
+ /** The version number of the standard. */
166
+ readonly version: 1;
167
+ /** The vendor name of the schema library. */
168
+ readonly vendor: string;
169
+ /** Inferred types associated with the schema. */
170
+ readonly types?: Types<Input, Output> | undefined;
171
+ }
172
+ /** The Standard Typed types interface. */
173
+ interface Types<Input = unknown, Output = Input> {
174
+ /** The input type of the schema. */
175
+ readonly input: Input;
176
+ /** The output type of the schema. */
177
+ readonly output: Output;
178
+ }
179
+ /** Infers the input type of a Standard Typed. */
180
+ type InferInput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["input"];
181
+ /** Infers the output type of a Standard Typed. */
182
+ type InferOutput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["output"];
183
+ }
184
+ /** The Standard Schema interface. */
185
+ interface StandardSchemaV1<Input = unknown, Output = Input> {
186
+ /** The Standard Schema properties. */
187
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
188
+ }
189
+ declare namespace StandardSchemaV1 {
190
+ /** The Standard Schema properties interface. */
191
+ interface Props<Input = unknown, Output = Input> extends StandardTypedV1.Props<Input, Output> {
192
+ /** Validates unknown input values. */
193
+ readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => Result<Output> | Promise<Result<Output>>;
194
+ }
195
+ /** The result interface of the validate function. */
196
+ type Result<Output> = SuccessResult<Output> | FailureResult;
197
+ /** The result interface if validation succeeds. */
198
+ interface SuccessResult<Output> {
199
+ /** The typed output value. */
200
+ readonly value: Output;
201
+ /** A falsy value for `issues` indicates success. */
202
+ readonly issues?: undefined;
203
+ }
204
+ interface Options {
205
+ /** Explicit support for additional vendor-specific parameters, if needed. */
206
+ readonly libraryOptions?: Record<string, unknown> | undefined;
207
+ }
208
+ /** The result interface if validation fails. */
209
+ interface FailureResult {
210
+ /** The issues of failed validation. */
211
+ readonly issues: ReadonlyArray<Issue>;
212
+ }
213
+ /** The issue interface of the failure output. */
214
+ interface Issue {
215
+ /** The error message of the issue. */
216
+ readonly message: string;
217
+ /** The path of the issue, if any. */
218
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
219
+ }
220
+ /** The path segment interface of the issue. */
221
+ interface PathSegment {
222
+ /** The key representing a path segment. */
223
+ readonly key: PropertyKey;
224
+ }
225
+ /** The Standard types interface. */
226
+ interface Types<Input = unknown, Output = Input> extends StandardTypedV1.Types<Input, Output> {}
227
+ /** Infers the input type of a Standard. */
228
+ type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
229
+ /** Infers the output type of a Standard. */
230
+ type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
231
+ }
232
+ /** The Standard JSON Schema interface. */
233
+ //#endregion
156
234
  //#region src/helper/redirect.d.ts
157
235
  /**
158
236
  * Generic HTTP redirect wrapped modeled after Spring's `RedirectView`.
@@ -308,7 +386,7 @@ declare enum HttpStatus {
308
386
  NETWORK_AUTHENTICATION_REQUIRED = 511
309
387
  }
310
388
  //#endregion
311
- //#region src/helper/error.d.ts
389
+ //#region src/helper/error/responsestatus.d.ts
312
390
  /**
313
391
  * Ensure that you define a middleware that can handle this error.
314
392
  *
@@ -319,6 +397,16 @@ declare class ResponseStatusError extends Error {
319
397
  constructor(status: HttpStatus, message?: string);
320
398
  }
321
399
  //#endregion
400
+ //#region src/helper/error/parse.d.ts
401
+ type ParserErrorLocation = "reqbody" | "reqparams" | "reqquery";
402
+ /**
403
+ * This error should be thrown when some data cannot be parsed by a given schema.
404
+ */
405
+ declare class ParserError extends ResponseStatusError {
406
+ constructor(location: ParserErrorLocation, issues: readonly StandardSchemaV1.Issue[], vendor: string);
407
+ private static formatMessage;
408
+ }
409
+ //#endregion
322
410
  //#region src/helper/sapling.d.ts
323
411
  /**
324
412
  * Collection of utility functions which are essential for Sapling to function.
@@ -338,7 +426,7 @@ declare class Sapling {
338
426
  * app.use(router);
339
427
  * ```
340
428
  */
341
- static resolve<TClass>(this: void, clazz: Class<TClass>): Router;
429
+ static resolve<TClass>(this: void, clazz: Class<TClass>): Router | ErrorRequestHandler;
342
430
  /**
343
431
  * Register this function as a middleware in order to utilize Sapling's `deserialize` function.
344
432
  *
@@ -365,29 +453,6 @@ declare class Sapling {
365
453
  * ```
366
454
  */
367
455
  static registerApp(app: e.Express): void;
368
- /**
369
- * Register a middleware that will handle {@link ResponseStatusError}.
370
- *
371
- * This middleware will chain to the next middleware if it does not catch {@link ResponseStatusError}.
372
- * You may still define middleware to handle all other errors in a separate `app.use` call.
373
- *
374
- * @example
375
- * ```ts
376
- * import express from "express";
377
- * import { Sapling } from "@tahminator/sapling";
378
- *
379
- * const app = express();
380
- *
381
- * Sapling.loadResponseStatusErrorMiddleware(app, (err, req, res, next) => {
382
- * // `err` is guaranteed to be of type ResponseStatusError
383
- * res.status(err.status).json({
384
- * success: false,
385
- * message: err.message,
386
- * });
387
- * });
388
- * ```
389
- */
390
- static loadResponseStatusErrorMiddleware(this: void, app: e.Express, fn: (err: ResponseStatusError, request: e.Request, response: e.Response, next: e.NextFunction) => void): void;
391
456
  /**
392
457
  * Serialize a value into a JSON string.
393
458
  *
@@ -418,4 +483,35 @@ declare class Sapling {
418
483
  static setDeserializeFn(this: void, fn: (value: string) => any): void;
419
484
  }
420
485
  //#endregion
421
- export { Class, Controller, DELETE, ExpressMiddlewareFn, ExpressRouterMethodKey, ExpressRouterMethods, GET, HEAD, Html404ErrorPage, HttpHeaders, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, RedirectView, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, RouteDefinition, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getRoutes, _resolve, methodResolve };
486
+ //#region src/annotation/request.d.ts
487
+ type RequestSchemaDefinition = {
488
+ body?: StandardSchemaV1;
489
+ param?: StandardSchemaV1;
490
+ query?: StandardSchemaV1;
491
+ };
492
+ declare function RequestBody(schema: StandardSchemaV1): MethodDecorator;
493
+ declare function RequestParam(schema: StandardSchemaV1): MethodDecorator;
494
+ declare function RequestQuery(schema: StandardSchemaV1): MethodDecorator;
495
+ declare function _getRequestSchemas(ctor: Function, fnName: string): RequestSchemaDefinition | undefined;
496
+ declare function _parseOrThrow<TSchema extends StandardSchemaV1>(schema: TSchema, input: unknown, kind: ParserErrorLocation): Promise<StandardSchemaV1.InferOutput<TSchema>>;
497
+ //#endregion
498
+ //#region src/middleware/default/base.d.ts
499
+ /**
500
+ * This should be registered last in the middleware chain.
501
+ *
502
+ * All exception messages are hidden from the request by default.
503
+ */
504
+ declare class DefaultBaseErrorMiddleware {
505
+ handle(err: unknown, _request: Request, _response: Response, _next: NextFunction): ResponseEntity<{
506
+ message: string;
507
+ }>;
508
+ }
509
+ //#endregion
510
+ //#region src/middleware/default/responsestatus.d.ts
511
+ declare class DefaultResponseStatusErrorMiddleware {
512
+ handle(err: unknown, _request: Request, _response: Response, _next: NextFunction): ResponseEntity<{
513
+ message: string;
514
+ }> | undefined;
515
+ }
516
+ //#endregion
517
+ export { Class, Controller, DELETE, DefaultBaseErrorMiddleware, DefaultResponseStatusErrorMiddleware, ExpressMiddlewareFn, ExpressRouterMethodKey, ExpressRouterMethods, GET, HEAD, Html404ErrorPage, HttpHeaders, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, ParserErrorLocation, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, RouteDefinition, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getRequestSchemas, _getRoutes, _parseOrThrow, _resolve, methodResolve };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import e, { NextFunction, Request, Response, Router } from "express";
1
+ import e, { ErrorRequestHandler, NextFunction, Request, Response, Router } from "express";
2
2
 
3
3
  //#region src/html/404.d.ts
4
4
  /**
@@ -29,7 +29,7 @@ type HttpHeaders = Record<string, string>;
29
29
  type ExpressMiddlewareFn = ($1: Request, $2: Response, $3: NextFunction) => void;
30
30
  //#endregion
31
31
  //#region src/annotation/controller.d.ts
32
- declare const _ControllerRegistry: WeakMap<Function, Router>;
32
+ declare const _ControllerRegistry: WeakMap<Function, Router | ErrorRequestHandler>;
33
33
  type ControllerProps = {
34
34
  /**
35
35
  * Optional URL prefix applied to all routes in the controller. Defaults to "".
@@ -153,6 +153,84 @@ declare function _getRoutes(ctor: Function): readonly RouteDefinition[];
153
153
  */
154
154
  declare function MiddlewareClass(...args: Parameters<typeof Controller>): ClassDecorator;
155
155
  //#endregion
156
+ //#region node_modules/.pnpm/@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec/dist/index.d.ts
157
+ /** The Standard Typed interface. This is a base type extended by other specs. */
158
+ interface StandardTypedV1<Input = unknown, Output = Input> {
159
+ /** The Standard properties. */
160
+ readonly "~standard": StandardTypedV1.Props<Input, Output>;
161
+ }
162
+ declare namespace StandardTypedV1 {
163
+ /** The Standard Typed properties interface. */
164
+ interface Props<Input = unknown, Output = Input> {
165
+ /** The version number of the standard. */
166
+ readonly version: 1;
167
+ /** The vendor name of the schema library. */
168
+ readonly vendor: string;
169
+ /** Inferred types associated with the schema. */
170
+ readonly types?: Types<Input, Output> | undefined;
171
+ }
172
+ /** The Standard Typed types interface. */
173
+ interface Types<Input = unknown, Output = Input> {
174
+ /** The input type of the schema. */
175
+ readonly input: Input;
176
+ /** The output type of the schema. */
177
+ readonly output: Output;
178
+ }
179
+ /** Infers the input type of a Standard Typed. */
180
+ type InferInput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["input"];
181
+ /** Infers the output type of a Standard Typed. */
182
+ type InferOutput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["output"];
183
+ }
184
+ /** The Standard Schema interface. */
185
+ interface StandardSchemaV1<Input = unknown, Output = Input> {
186
+ /** The Standard Schema properties. */
187
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
188
+ }
189
+ declare namespace StandardSchemaV1 {
190
+ /** The Standard Schema properties interface. */
191
+ interface Props<Input = unknown, Output = Input> extends StandardTypedV1.Props<Input, Output> {
192
+ /** Validates unknown input values. */
193
+ readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => Result<Output> | Promise<Result<Output>>;
194
+ }
195
+ /** The result interface of the validate function. */
196
+ type Result<Output> = SuccessResult<Output> | FailureResult;
197
+ /** The result interface if validation succeeds. */
198
+ interface SuccessResult<Output> {
199
+ /** The typed output value. */
200
+ readonly value: Output;
201
+ /** A falsy value for `issues` indicates success. */
202
+ readonly issues?: undefined;
203
+ }
204
+ interface Options {
205
+ /** Explicit support for additional vendor-specific parameters, if needed. */
206
+ readonly libraryOptions?: Record<string, unknown> | undefined;
207
+ }
208
+ /** The result interface if validation fails. */
209
+ interface FailureResult {
210
+ /** The issues of failed validation. */
211
+ readonly issues: ReadonlyArray<Issue>;
212
+ }
213
+ /** The issue interface of the failure output. */
214
+ interface Issue {
215
+ /** The error message of the issue. */
216
+ readonly message: string;
217
+ /** The path of the issue, if any. */
218
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
219
+ }
220
+ /** The path segment interface of the issue. */
221
+ interface PathSegment {
222
+ /** The key representing a path segment. */
223
+ readonly key: PropertyKey;
224
+ }
225
+ /** The Standard types interface. */
226
+ interface Types<Input = unknown, Output = Input> extends StandardTypedV1.Types<Input, Output> {}
227
+ /** Infers the input type of a Standard. */
228
+ type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
229
+ /** Infers the output type of a Standard. */
230
+ type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
231
+ }
232
+ /** The Standard JSON Schema interface. */
233
+ //#endregion
156
234
  //#region src/helper/redirect.d.ts
157
235
  /**
158
236
  * Generic HTTP redirect wrapped modeled after Spring's `RedirectView`.
@@ -308,7 +386,7 @@ declare enum HttpStatus {
308
386
  NETWORK_AUTHENTICATION_REQUIRED = 511
309
387
  }
310
388
  //#endregion
311
- //#region src/helper/error.d.ts
389
+ //#region src/helper/error/responsestatus.d.ts
312
390
  /**
313
391
  * Ensure that you define a middleware that can handle this error.
314
392
  *
@@ -319,6 +397,16 @@ declare class ResponseStatusError extends Error {
319
397
  constructor(status: HttpStatus, message?: string);
320
398
  }
321
399
  //#endregion
400
+ //#region src/helper/error/parse.d.ts
401
+ type ParserErrorLocation = "reqbody" | "reqparams" | "reqquery";
402
+ /**
403
+ * This error should be thrown when some data cannot be parsed by a given schema.
404
+ */
405
+ declare class ParserError extends ResponseStatusError {
406
+ constructor(location: ParserErrorLocation, issues: readonly StandardSchemaV1.Issue[], vendor: string);
407
+ private static formatMessage;
408
+ }
409
+ //#endregion
322
410
  //#region src/helper/sapling.d.ts
323
411
  /**
324
412
  * Collection of utility functions which are essential for Sapling to function.
@@ -338,7 +426,7 @@ declare class Sapling {
338
426
  * app.use(router);
339
427
  * ```
340
428
  */
341
- static resolve<TClass>(this: void, clazz: Class<TClass>): Router;
429
+ static resolve<TClass>(this: void, clazz: Class<TClass>): Router | ErrorRequestHandler;
342
430
  /**
343
431
  * Register this function as a middleware in order to utilize Sapling's `deserialize` function.
344
432
  *
@@ -365,29 +453,6 @@ declare class Sapling {
365
453
  * ```
366
454
  */
367
455
  static registerApp(app: e.Express): void;
368
- /**
369
- * Register a middleware that will handle {@link ResponseStatusError}.
370
- *
371
- * This middleware will chain to the next middleware if it does not catch {@link ResponseStatusError}.
372
- * You may still define middleware to handle all other errors in a separate `app.use` call.
373
- *
374
- * @example
375
- * ```ts
376
- * import express from "express";
377
- * import { Sapling } from "@tahminator/sapling";
378
- *
379
- * const app = express();
380
- *
381
- * Sapling.loadResponseStatusErrorMiddleware(app, (err, req, res, next) => {
382
- * // `err` is guaranteed to be of type ResponseStatusError
383
- * res.status(err.status).json({
384
- * success: false,
385
- * message: err.message,
386
- * });
387
- * });
388
- * ```
389
- */
390
- static loadResponseStatusErrorMiddleware(this: void, app: e.Express, fn: (err: ResponseStatusError, request: e.Request, response: e.Response, next: e.NextFunction) => void): void;
391
456
  /**
392
457
  * Serialize a value into a JSON string.
393
458
  *
@@ -418,4 +483,35 @@ declare class Sapling {
418
483
  static setDeserializeFn(this: void, fn: (value: string) => any): void;
419
484
  }
420
485
  //#endregion
421
- export { Class, Controller, DELETE, ExpressMiddlewareFn, ExpressRouterMethodKey, ExpressRouterMethods, GET, HEAD, Html404ErrorPage, HttpHeaders, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, RedirectView, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, RouteDefinition, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getRoutes, _resolve, methodResolve };
486
+ //#region src/annotation/request.d.ts
487
+ type RequestSchemaDefinition = {
488
+ body?: StandardSchemaV1;
489
+ param?: StandardSchemaV1;
490
+ query?: StandardSchemaV1;
491
+ };
492
+ declare function RequestBody(schema: StandardSchemaV1): MethodDecorator;
493
+ declare function RequestParam(schema: StandardSchemaV1): MethodDecorator;
494
+ declare function RequestQuery(schema: StandardSchemaV1): MethodDecorator;
495
+ declare function _getRequestSchemas(ctor: Function, fnName: string): RequestSchemaDefinition | undefined;
496
+ declare function _parseOrThrow<TSchema extends StandardSchemaV1>(schema: TSchema, input: unknown, kind: ParserErrorLocation): Promise<StandardSchemaV1.InferOutput<TSchema>>;
497
+ //#endregion
498
+ //#region src/middleware/default/base.d.ts
499
+ /**
500
+ * This should be registered last in the middleware chain.
501
+ *
502
+ * All exception messages are hidden from the request by default.
503
+ */
504
+ declare class DefaultBaseErrorMiddleware {
505
+ handle(err: unknown, _request: Request, _response: Response, _next: NextFunction): ResponseEntity<{
506
+ message: string;
507
+ }>;
508
+ }
509
+ //#endregion
510
+ //#region src/middleware/default/responsestatus.d.ts
511
+ declare class DefaultResponseStatusErrorMiddleware {
512
+ handle(err: unknown, _request: Request, _response: Response, _next: NextFunction): ResponseEntity<{
513
+ message: string;
514
+ }> | undefined;
515
+ }
516
+ //#endregion
517
+ export { Class, Controller, DELETE, DefaultBaseErrorMiddleware, DefaultResponseStatusErrorMiddleware, ExpressMiddlewareFn, ExpressRouterMethodKey, ExpressRouterMethods, GET, HEAD, Html404ErrorPage, HttpHeaders, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, ParserErrorLocation, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, RouteDefinition, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getRequestSchemas, _getRoutes, _parseOrThrow, _resolve, methodResolve };
package/dist/index.mjs CHANGED
@@ -197,6 +197,46 @@ var ResponseEntityBuilder = class {
197
197
  }
198
198
  };
199
199
  //#endregion
200
+ //#region src/helper/error/responsestatus.ts
201
+ /**
202
+ * Ensure that you define a middleware that can handle this error.
203
+ *
204
+ * @see {@link Sapling.loadResponseStatusErrorMiddleware}
205
+ */
206
+ var ResponseStatusError = class ResponseStatusError extends Error {
207
+ constructor(status, message) {
208
+ super(message ?? "Something went wrong.");
209
+ this.status = status;
210
+ Object.setPrototypeOf(this, new.target.prototype);
211
+ this.name = `HttpError(${HttpStatus[status]})`;
212
+ if (Error.captureStackTrace) Error.captureStackTrace(this, ResponseStatusError);
213
+ }
214
+ };
215
+ //#endregion
216
+ //#region src/helper/error/parse.ts
217
+ /**
218
+ * This error should be thrown when some data cannot be parsed by a given schema.
219
+ */
220
+ var ParserError = class ParserError extends ResponseStatusError {
221
+ constructor(location, issues, vendor) {
222
+ super(400, ParserError.formatMessage(location, issues, vendor));
223
+ Object.setPrototypeOf(this, new.target.prototype);
224
+ }
225
+ static formatMessage(location, issues, vendor) {
226
+ const formatted = issues.map((i) => {
227
+ const path = Array.isArray(i.path) ? i.path.map((seg) => typeof seg === "object" && seg ? String(seg.key) : String(seg)).join(".") : "";
228
+ return path ? `${path}: ${i.message}` : i.message;
229
+ }).join("; ");
230
+ return `${vendor} failed to parse ${(() => {
231
+ switch (location) {
232
+ case "reqbody": return "request body";
233
+ case "reqparams": return "request params";
234
+ case "reqquery": return "request query";
235
+ }
236
+ })()}: ${formatted}`;
237
+ }
238
+ };
239
+ //#endregion
200
240
  //#region src/helper/sapling.ts
201
241
  const settings = {
202
242
  serialize: JSON.stringify,
@@ -270,34 +310,6 @@ var Sapling = class Sapling {
270
310
  app.use(Sapling.json());
271
311
  }
272
312
  /**
273
- * Register a middleware that will handle {@link ResponseStatusError}.
274
- *
275
- * This middleware will chain to the next middleware if it does not catch {@link ResponseStatusError}.
276
- * You may still define middleware to handle all other errors in a separate `app.use` call.
277
- *
278
- * @example
279
- * ```ts
280
- * import express from "express";
281
- * import { Sapling } from "@tahminator/sapling";
282
- *
283
- * const app = express();
284
- *
285
- * Sapling.loadResponseStatusErrorMiddleware(app, (err, req, res, next) => {
286
- * // `err` is guaranteed to be of type ResponseStatusError
287
- * res.status(err.status).json({
288
- * success: false,
289
- * message: err.message,
290
- * });
291
- * });
292
- * ```
293
- */
294
- static loadResponseStatusErrorMiddleware(app, fn) {
295
- app.use(((err, req, res, next) => {
296
- if (err instanceof ResponseStatusError) fn(err, req, res, next);
297
- else next(err);
298
- }));
299
- }
300
- /**
301
313
  * Serialize a value into a JSON string.
302
314
  *
303
315
  * This function is used in {@link ResponseEntity} to serialize the `body`.
@@ -335,22 +347,6 @@ var Sapling = class Sapling {
335
347
  }
336
348
  };
337
349
  //#endregion
338
- //#region src/helper/error.ts
339
- /**
340
- * Ensure that you define a middleware that can handle this error.
341
- *
342
- * @see {@link Sapling.loadResponseStatusErrorMiddleware}
343
- */
344
- var ResponseStatusError = class ResponseStatusError extends Error {
345
- constructor(status, message) {
346
- super(message ?? "Something went wrong.");
347
- this.status = status;
348
- Object.setPrototypeOf(this, new.target.prototype);
349
- this.name = `HttpError(${HttpStatus[status]})`;
350
- if (Error.captureStackTrace) Error.captureStackTrace(this, ResponseStatusError);
351
- }
352
- };
353
- //#endregion
354
350
  //#region src/types.ts
355
351
  const methodResolve = {
356
352
  GET: "get",
@@ -483,6 +479,56 @@ function _resolve(ctor) {
483
479
  return _InjectableRegistry.get(ctor);
484
480
  }
485
481
  //#endregion
482
+ //#region src/annotation/request.ts
483
+ const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
484
+ function _getOrCreateRequestSchemaDefinition(ctor, fnName) {
485
+ const byFn = (() => {
486
+ const fn = _requestSchemaStore.get(ctor);
487
+ if (fn) return fn;
488
+ const newFn = /* @__PURE__ */ new Map();
489
+ _requestSchemaStore.set(ctor, newFn);
490
+ return newFn;
491
+ })();
492
+ const existing = byFn.get(fnName);
493
+ if (existing) return existing;
494
+ const created = {};
495
+ byFn.set(fnName, created);
496
+ return created;
497
+ }
498
+ function _setOnce(def, key, schema, fnName) {
499
+ if (def[key]) throw new Error(`Duplicate request schema for "${String(key)}" on method "${fnName}"`);
500
+ def[key] = schema;
501
+ }
502
+ function RequestBody(schema) {
503
+ return (target, propertyKey) => {
504
+ const ctor = target.constructor;
505
+ const fnName = String(propertyKey);
506
+ _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "body", schema, fnName);
507
+ };
508
+ }
509
+ function RequestParam(schema) {
510
+ return (target, propertyKey) => {
511
+ const ctor = target.constructor;
512
+ const fnName = String(propertyKey);
513
+ _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "param", schema, fnName);
514
+ };
515
+ }
516
+ function RequestQuery(schema) {
517
+ return (target, propertyKey) => {
518
+ const ctor = target.constructor;
519
+ const fnName = String(propertyKey);
520
+ _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "query", schema, fnName);
521
+ };
522
+ }
523
+ function _getRequestSchemas(ctor, fnName) {
524
+ return _requestSchemaStore.get(ctor)?.get(fnName);
525
+ }
526
+ async function _parseOrThrow(schema, input, kind) {
527
+ const result = await schema["~standard"].validate(input);
528
+ if (result.issues) throw new ParserError(kind, result.issues, schema["~standard"].vendor);
529
+ return result.value;
530
+ }
531
+ //#endregion
486
532
  //#region src/annotation/route.ts
487
533
  const _routeStore = /* @__PURE__ */ new WeakMap();
488
534
  /**
@@ -580,6 +626,14 @@ function Controller({ prefix = "", deps = [] } = {}) {
580
626
  const usedRoutes = /* @__PURE__ */ new Set();
581
627
  _InjectableDeps.set(targetClass, deps);
582
628
  const controllerInstance = _resolve(targetClass);
629
+ if (routes.reduce((prev, r) => {
630
+ if (r.method !== "USE") return prev;
631
+ const fn = controllerInstance[r.fnName];
632
+ return typeof fn === "function" && fn.length >= 4 ? prev + 1 : prev;
633
+ }, 0) > 1) throw new Error(`Invalid @MiddlewareClass class "${targetClass.name}":
634
+ Multiple 4-arg @Middleware() error handlers were found.
635
+ Express will not enter routers in error mode, so an error-middleware class must expose exactly one error handler.
636
+ Split these into separate @MiddlewareClass classes, or merge the logic into a single method.`);
583
637
  for (const { method, path, fnName } of routes) {
584
638
  const fn = controllerInstance[fnName];
585
639
  if (typeof fn !== "function") continue;
@@ -588,9 +642,34 @@ function Controller({ prefix = "", deps = [] } = {}) {
588
642
  if (method !== "USE" && usedRoutes.has(routeKey)) throw new Error(`Duplicate route [${method}] "${path instanceof RegExp ? path.source : fp}" detected in controller "${target.name}"`);
589
643
  if (method !== "USE") usedRoutes.add(routeKey);
590
644
  const methodName = methodResolve[method];
645
+ if (method === "USE" && fn.length >= 4) {
646
+ const middlewareFn = async (err, request, response, next) => {
647
+ try {
648
+ const result = fn.bind(controllerInstance)(err, request, response, next);
649
+ if (result instanceof ResponseEntity) {
650
+ response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
651
+ return;
652
+ }
653
+ if (result instanceof RedirectView) {
654
+ response.redirect(result.getUrl());
655
+ return;
656
+ }
657
+ } catch (e) {
658
+ console.error(e);
659
+ next(e);
660
+ }
661
+ };
662
+ _ControllerRegistry.set(targetClass, middlewareFn);
663
+ return;
664
+ }
591
665
  router[methodName](fp, async (request, response, next) => {
666
+ const schemas = _getRequestSchemas(target, fnName);
667
+ if (schemas) {
668
+ if (schemas.body) request.body = await _parseOrThrow(schemas.body, request.body, "reqbody");
669
+ if (schemas.param) request.params = await _parseOrThrow(schemas.param, request.params, "reqparams");
670
+ if (schemas.query) request.query = await _parseOrThrow(schemas.query, request.query, "reqquery");
671
+ }
592
672
  const result = await fn.bind(controllerInstance)(request, response, next);
593
- if (method === "USE") return;
594
673
  if (result instanceof ResponseEntity) {
595
674
  response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
596
675
  return;
@@ -599,7 +678,7 @@ function Controller({ prefix = "", deps = [] } = {}) {
599
678
  response.redirect(result.getUrl());
600
679
  return;
601
680
  }
602
- if (!response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${methodName.toUpperCase()} ${path instanceof RegExp ? path.source : fp}`));
681
+ if (method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${methodName.toUpperCase()} ${path instanceof RegExp ? path.source : fp}`));
603
682
  });
604
683
  }
605
684
  _ControllerRegistry.set(targetClass, router);
@@ -618,4 +697,31 @@ function MiddlewareClass(...args) {
618
697
  return Controller(...args);
619
698
  }
620
699
  //#endregion
621
- export { Controller, DELETE, GET, HEAD, Html404ErrorPage, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, RedirectView, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getRoutes, _resolve, methodResolve };
700
+ //#region \0@oxc-project+runtime@0.127.0/helpers/decorate.js
701
+ function __decorate(decorators, target, key, desc) {
702
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
703
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
704
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
705
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
706
+ }
707
+ //#endregion
708
+ //#region src/middleware/default/base.ts
709
+ let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
710
+ handle(err, _request, _response, _next) {
711
+ console.error("[Error]", err);
712
+ return ResponseEntity.status(500).body({ message: "Internal Server Error" });
713
+ }
714
+ };
715
+ __decorate([Middleware()], DefaultBaseErrorMiddleware.prototype, "handle", null);
716
+ DefaultBaseErrorMiddleware = __decorate([MiddlewareClass()], DefaultBaseErrorMiddleware);
717
+ //#endregion
718
+ //#region src/middleware/default/responsestatus.ts
719
+ let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddleware {
720
+ handle(err, _request, _response, _next) {
721
+ if (err instanceof ResponseStatusError) return ResponseEntity.status(err.status).body({ message: err.message });
722
+ }
723
+ };
724
+ __decorate([Middleware()], DefaultResponseStatusErrorMiddleware.prototype, "handle", null);
725
+ DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultResponseStatusErrorMiddleware);
726
+ //#endregion
727
+ export { Controller, DELETE, DefaultBaseErrorMiddleware, DefaultResponseStatusErrorMiddleware, GET, HEAD, Html404ErrorPage, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getRequestSchemas, _getRoutes, _parseOrThrow, _resolve, methodResolve };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tahminator/sapling",
3
- "version": "1.5.28",
3
+ "version": "2.0.1",
4
4
  "author": "Tahmid Ahmed",
5
5
  "description": "A library to help you write cleaner Express.js code",
6
6
  "repository": {
@@ -41,6 +41,7 @@
41
41
  },
42
42
  "devDependencies": {
43
43
  "@eslint/js": "^10.0.1",
44
+ "@standard-schema/spec": "^1.1.0",
44
45
  "@types/express": "^5",
45
46
  "@types/supertest": "^7.2.0",
46
47
  "@vitest/coverage-istanbul": "^4.1.2",
@@ -55,6 +56,10 @@
55
56
  "tsdown": "^0.21.10",
56
57
  "typescript-eslint": "^8.57.2",
57
58
  "vite-tsconfig-paths": "^6.1.1",
58
- "vitest": "^4.1.2"
59
+ "vitest": "^4.1.2",
60
+ "zod": "^4.4.3"
61
+ },
62
+ "inlinedDependencies": {
63
+ "@standard-schema/spec": "1.1.0"
59
64
  }
60
65
  }