@tahminator/sapling 1.5.28-beta.d9aa39dd → 1.5.28-beta.f1a8f6ae

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/dist/index.cjs CHANGED
@@ -310,34 +310,6 @@ var Sapling = class Sapling {
310
310
  app.use(Sapling.json());
311
311
  }
312
312
  /**
313
- * Register a middleware that will handle {@link ResponseStatusError}.
314
- *
315
- * This middleware will chain to the next middleware if it does not catch {@link ResponseStatusError}.
316
- * You may still define middleware to handle all other errors in a separate `app.use` call.
317
- *
318
- * @example
319
- * ```ts
320
- * import express from "express";
321
- * import { Sapling } from "@tahminator/sapling";
322
- *
323
- * const app = express();
324
- *
325
- * Sapling.loadResponseStatusErrorMiddleware(app, (err, req, res, next) => {
326
- * // `err` is guaranteed to be of type ResponseStatusError
327
- * res.status(err.status).json({
328
- * success: false,
329
- * message: err.message,
330
- * });
331
- * });
332
- * ```
333
- */
334
- static loadResponseStatusErrorMiddleware(app, fn) {
335
- app.use(((err, req, res, next) => {
336
- if (err instanceof ResponseStatusError) fn(err, req, res, next);
337
- else next(err);
338
- }));
339
- }
340
- /**
341
313
  * Serialize a value into a JSON string.
342
314
  *
343
315
  * This function is used in {@link ResponseEntity} to serialize the `body`.
@@ -678,6 +650,14 @@ function Controller({ prefix = "", deps = [] } = {}) {
678
650
  const usedRoutes = /* @__PURE__ */ new Set();
679
651
  _InjectableDeps.set(targetClass, deps);
680
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.`);
681
661
  for (const { method, path, fnName } of routes) {
682
662
  const fn = controllerInstance[fnName];
683
663
  if (typeof fn !== "function") continue;
@@ -686,6 +666,26 @@ function Controller({ prefix = "", deps = [] } = {}) {
686
666
  if (method !== "USE" && usedRoutes.has(routeKey)) throw new Error(`Duplicate route [${method}] "${path instanceof RegExp ? path.source : fp}" detected in controller "${target.name}"`);
687
667
  if (method !== "USE") usedRoutes.add(routeKey);
688
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
+ }
689
689
  router[methodName](fp, async (request, response, next) => {
690
690
  const schemas = _getRequestSchemas(target, fnName);
691
691
  if (schemas) {
@@ -694,7 +694,6 @@ function Controller({ prefix = "", deps = [] } = {}) {
694
694
  if (schemas.query) request.query = await _parseOrThrow(schemas.query, request.query, "reqquery");
695
695
  }
696
696
  const result = await fn.bind(controllerInstance)(request, response, next);
697
- if (method === "USE") return;
698
697
  if (result instanceof ResponseEntity) {
699
698
  response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
700
699
  return;
@@ -703,7 +702,7 @@ function Controller({ prefix = "", deps = [] } = {}) {
703
702
  response.redirect(result.getUrl());
704
703
  return;
705
704
  }
706
- 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}`));
707
706
  });
708
707
  }
709
708
  _ControllerRegistry.set(targetClass, router);
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 "".
@@ -416,7 +416,7 @@ declare class Sapling {
416
416
  * app.use(router);
417
417
  * ```
418
418
  */
419
- static resolve<TClass>(this: void, clazz: Class<TClass>): Router;
419
+ static resolve<TClass>(this: void, clazz: Class<TClass>): Router | ErrorRequestHandler;
420
420
  /**
421
421
  * Register this function as a middleware in order to utilize Sapling's `deserialize` function.
422
422
  *
@@ -443,29 +443,6 @@ declare class Sapling {
443
443
  * ```
444
444
  */
445
445
  static registerApp(app: e.Express): void;
446
- /**
447
- * Register a middleware that will handle {@link ResponseStatusError}.
448
- *
449
- * This middleware will chain to the next middleware if it does not catch {@link ResponseStatusError}.
450
- * You may still define middleware to handle all other errors in a separate `app.use` call.
451
- *
452
- * @example
453
- * ```ts
454
- * import express from "express";
455
- * import { Sapling } from "@tahminator/sapling";
456
- *
457
- * const app = express();
458
- *
459
- * Sapling.loadResponseStatusErrorMiddleware(app, (err, req, res, next) => {
460
- * // `err` is guaranteed to be of type ResponseStatusError
461
- * res.status(err.status).json({
462
- * success: false,
463
- * message: err.message,
464
- * });
465
- * });
466
- * ```
467
- */
468
- static loadResponseStatusErrorMiddleware(this: void, app: e.Express, fn: (err: ResponseStatusError, request: e.Request, response: e.Response, next: e.NextFunction) => void): void;
469
446
  /**
470
447
  * Serialize a value into a JSON string.
471
448
  *
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 "".
@@ -416,7 +416,7 @@ declare class Sapling {
416
416
  * app.use(router);
417
417
  * ```
418
418
  */
419
- static resolve<TClass>(this: void, clazz: Class<TClass>): Router;
419
+ static resolve<TClass>(this: void, clazz: Class<TClass>): Router | ErrorRequestHandler;
420
420
  /**
421
421
  * Register this function as a middleware in order to utilize Sapling's `deserialize` function.
422
422
  *
@@ -443,29 +443,6 @@ declare class Sapling {
443
443
  * ```
444
444
  */
445
445
  static registerApp(app: e.Express): void;
446
- /**
447
- * Register a middleware that will handle {@link ResponseStatusError}.
448
- *
449
- * This middleware will chain to the next middleware if it does not catch {@link ResponseStatusError}.
450
- * You may still define middleware to handle all other errors in a separate `app.use` call.
451
- *
452
- * @example
453
- * ```ts
454
- * import express from "express";
455
- * import { Sapling } from "@tahminator/sapling";
456
- *
457
- * const app = express();
458
- *
459
- * Sapling.loadResponseStatusErrorMiddleware(app, (err, req, res, next) => {
460
- * // `err` is guaranteed to be of type ResponseStatusError
461
- * res.status(err.status).json({
462
- * success: false,
463
- * message: err.message,
464
- * });
465
- * });
466
- * ```
467
- */
468
- static loadResponseStatusErrorMiddleware(this: void, app: e.Express, fn: (err: ResponseStatusError, request: e.Request, response: e.Response, next: e.NextFunction) => void): void;
469
446
  /**
470
447
  * Serialize a value into a JSON string.
471
448
  *
package/dist/index.mjs CHANGED
@@ -286,34 +286,6 @@ var Sapling = class Sapling {
286
286
  app.use(Sapling.json());
287
287
  }
288
288
  /**
289
- * Register a middleware that will handle {@link ResponseStatusError}.
290
- *
291
- * This middleware will chain to the next middleware if it does not catch {@link ResponseStatusError}.
292
- * You may still define middleware to handle all other errors in a separate `app.use` call.
293
- *
294
- * @example
295
- * ```ts
296
- * import express from "express";
297
- * import { Sapling } from "@tahminator/sapling";
298
- *
299
- * const app = express();
300
- *
301
- * Sapling.loadResponseStatusErrorMiddleware(app, (err, req, res, next) => {
302
- * // `err` is guaranteed to be of type ResponseStatusError
303
- * res.status(err.status).json({
304
- * success: false,
305
- * message: err.message,
306
- * });
307
- * });
308
- * ```
309
- */
310
- static loadResponseStatusErrorMiddleware(app, fn) {
311
- app.use(((err, req, res, next) => {
312
- if (err instanceof ResponseStatusError) fn(err, req, res, next);
313
- else next(err);
314
- }));
315
- }
316
- /**
317
289
  * Serialize a value into a JSON string.
318
290
  *
319
291
  * This function is used in {@link ResponseEntity} to serialize the `body`.
@@ -654,6 +626,14 @@ function Controller({ prefix = "", deps = [] } = {}) {
654
626
  const usedRoutes = /* @__PURE__ */ new Set();
655
627
  _InjectableDeps.set(targetClass, deps);
656
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.`);
657
637
  for (const { method, path, fnName } of routes) {
658
638
  const fn = controllerInstance[fnName];
659
639
  if (typeof fn !== "function") continue;
@@ -662,6 +642,26 @@ function Controller({ prefix = "", deps = [] } = {}) {
662
642
  if (method !== "USE" && usedRoutes.has(routeKey)) throw new Error(`Duplicate route [${method}] "${path instanceof RegExp ? path.source : fp}" detected in controller "${target.name}"`);
663
643
  if (method !== "USE") usedRoutes.add(routeKey);
664
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
+ }
665
665
  router[methodName](fp, async (request, response, next) => {
666
666
  const schemas = _getRequestSchemas(target, fnName);
667
667
  if (schemas) {
@@ -670,7 +670,6 @@ function Controller({ prefix = "", deps = [] } = {}) {
670
670
  if (schemas.query) request.query = await _parseOrThrow(schemas.query, request.query, "reqquery");
671
671
  }
672
672
  const result = await fn.bind(controllerInstance)(request, response, next);
673
- if (method === "USE") return;
674
673
  if (result instanceof ResponseEntity) {
675
674
  response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
676
675
  return;
@@ -679,7 +678,7 @@ function Controller({ prefix = "", deps = [] } = {}) {
679
678
  response.redirect(result.getUrl());
680
679
  return;
681
680
  }
682
- 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}`));
683
682
  });
684
683
  }
685
684
  _ControllerRegistry.set(targetClass, router);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tahminator/sapling",
3
- "version": "1.5.28-beta.d9aa39dd",
3
+ "version": "1.5.28-beta.f1a8f6ae",
4
4
  "author": "Tahmid Ahmed",
5
5
  "description": "A library to help you write cleaner Express.js code",
6
6
  "repository": {
@@ -56,7 +56,8 @@
56
56
  "tsdown": "^0.21.10",
57
57
  "typescript-eslint": "^8.57.2",
58
58
  "vite-tsconfig-paths": "^6.1.1",
59
- "vitest": "^4.1.2"
59
+ "vitest": "^4.1.2",
60
+ "zod": "^4.4.3"
60
61
  },
61
62
  "inlinedDependencies": {
62
63
  "@standard-schema/spec": "1.1.0"