@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 +55 -1
- package/dist/index.cjs +170 -46
- package/dist/index.d.cts +124 -28
- package/dist/index.d.mts +124 -28
- package/dist/index.mjs +153 -47
- package/package.json +7 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
}
|