@tahminator/sapling 1.5.28 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -1
- package/dist/index.cjs +131 -46
- package/dist/index.d.cts +105 -28
- package/dist/index.d.mts +105 -28
- package/dist/index.mjs +126 -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,22 @@ 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
|
|
224
240
|
//#region src/helper/sapling.ts
|
|
225
241
|
const settings = {
|
|
226
242
|
serialize: JSON.stringify,
|
|
@@ -294,34 +310,6 @@ var Sapling = class Sapling {
|
|
|
294
310
|
app.use(Sapling.json());
|
|
295
311
|
}
|
|
296
312
|
/**
|
|
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
313
|
* Serialize a value into a JSON string.
|
|
326
314
|
*
|
|
327
315
|
* This function is used in {@link ResponseEntity} to serialize the `body`.
|
|
@@ -359,22 +347,6 @@ var Sapling = class Sapling {
|
|
|
359
347
|
}
|
|
360
348
|
};
|
|
361
349
|
//#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
350
|
//#region src/types.ts
|
|
379
351
|
const methodResolve = {
|
|
380
352
|
GET: "get",
|
|
@@ -507,6 +479,80 @@ function _resolve(ctor) {
|
|
|
507
479
|
return _InjectableRegistry.get(ctor);
|
|
508
480
|
}
|
|
509
481
|
//#endregion
|
|
482
|
+
//#region src/helper/error/exception.ts
|
|
483
|
+
/**
|
|
484
|
+
* This error should be thrown when some data cannot be parsed by a given schema.
|
|
485
|
+
*/
|
|
486
|
+
var ParserError = class ParserError extends ResponseStatusError {
|
|
487
|
+
constructor(location, issues, vendor) {
|
|
488
|
+
super(400, ParserError.formatMessage(location, issues, vendor));
|
|
489
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
490
|
+
}
|
|
491
|
+
static formatMessage(location, issues, vendor) {
|
|
492
|
+
const formatted = issues.map((i) => {
|
|
493
|
+
const path = Array.isArray(i.path) ? i.path.map((seg) => typeof seg === "object" && seg ? String(seg.key) : String(seg)).join(".") : "";
|
|
494
|
+
return path ? `${path}: ${i.message}` : i.message;
|
|
495
|
+
}).join("; ");
|
|
496
|
+
return `${vendor} failed to parse ${(() => {
|
|
497
|
+
switch (location) {
|
|
498
|
+
case "reqbody": return "request body";
|
|
499
|
+
case "reqparams": return "request params";
|
|
500
|
+
case "reqquery": return "request query";
|
|
501
|
+
}
|
|
502
|
+
})()}: ${formatted}`;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
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);
|
|
@@ -655,7 +734,11 @@ exports.OPTIONS = OPTIONS;
|
|
|
655
734
|
exports.PATCH = PATCH;
|
|
656
735
|
exports.POST = POST;
|
|
657
736
|
exports.PUT = PUT;
|
|
737
|
+
exports.ParserError = ParserError;
|
|
658
738
|
exports.RedirectView = RedirectView;
|
|
739
|
+
exports.RequestBody = RequestBody;
|
|
740
|
+
exports.RequestParam = RequestParam;
|
|
741
|
+
exports.RequestQuery = RequestQuery;
|
|
659
742
|
exports.ResponseEntity = ResponseEntity;
|
|
660
743
|
exports.ResponseEntityBuilder = ResponseEntityBuilder;
|
|
661
744
|
exports.ResponseStatusError = ResponseStatusError;
|
|
@@ -664,6 +747,8 @@ exports._ControllerRegistry = _ControllerRegistry;
|
|
|
664
747
|
exports._InjectableDeps = _InjectableDeps;
|
|
665
748
|
exports._InjectableRegistry = _InjectableRegistry;
|
|
666
749
|
exports._Route = _Route;
|
|
750
|
+
exports._getRequestSchemas = _getRequestSchemas;
|
|
667
751
|
exports._getRoutes = _getRoutes;
|
|
752
|
+
exports._parseOrThrow = _parseOrThrow;
|
|
668
753
|
exports._resolve = _resolve;
|
|
669
754
|
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
|
*
|
|
@@ -338,7 +416,7 @@ declare class Sapling {
|
|
|
338
416
|
* app.use(router);
|
|
339
417
|
* ```
|
|
340
418
|
*/
|
|
341
|
-
static resolve<TClass>(this: void, clazz: Class<TClass>): Router;
|
|
419
|
+
static resolve<TClass>(this: void, clazz: Class<TClass>): Router | ErrorRequestHandler;
|
|
342
420
|
/**
|
|
343
421
|
* Register this function as a middleware in order to utilize Sapling's `deserialize` function.
|
|
344
422
|
*
|
|
@@ -365,29 +443,6 @@ declare class Sapling {
|
|
|
365
443
|
* ```
|
|
366
444
|
*/
|
|
367
445
|
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
446
|
/**
|
|
392
447
|
* Serialize a value into a JSON string.
|
|
393
448
|
*
|
|
@@ -418,4 +473,26 @@ declare class Sapling {
|
|
|
418
473
|
static setDeserializeFn(this: void, fn: (value: string) => any): void;
|
|
419
474
|
}
|
|
420
475
|
//#endregion
|
|
421
|
-
|
|
476
|
+
//#region src/helper/error/exception.d.ts
|
|
477
|
+
type ParserErrorLocation = "reqbody" | "reqparams" | "reqquery";
|
|
478
|
+
/**
|
|
479
|
+
* This error should be thrown when some data cannot be parsed by a given schema.
|
|
480
|
+
*/
|
|
481
|
+
declare class ParserError extends ResponseStatusError {
|
|
482
|
+
constructor(location: ParserErrorLocation, issues: readonly StandardSchemaV1.Issue[], vendor: string);
|
|
483
|
+
private static formatMessage;
|
|
484
|
+
}
|
|
485
|
+
//#endregion
|
|
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
|
+
export { Class, Controller, DELETE, 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
|
*
|
|
@@ -338,7 +416,7 @@ declare class Sapling {
|
|
|
338
416
|
* app.use(router);
|
|
339
417
|
* ```
|
|
340
418
|
*/
|
|
341
|
-
static resolve<TClass>(this: void, clazz: Class<TClass>): Router;
|
|
419
|
+
static resolve<TClass>(this: void, clazz: Class<TClass>): Router | ErrorRequestHandler;
|
|
342
420
|
/**
|
|
343
421
|
* Register this function as a middleware in order to utilize Sapling's `deserialize` function.
|
|
344
422
|
*
|
|
@@ -365,29 +443,6 @@ declare class Sapling {
|
|
|
365
443
|
* ```
|
|
366
444
|
*/
|
|
367
445
|
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
446
|
/**
|
|
392
447
|
* Serialize a value into a JSON string.
|
|
393
448
|
*
|
|
@@ -418,4 +473,26 @@ declare class Sapling {
|
|
|
418
473
|
static setDeserializeFn(this: void, fn: (value: string) => any): void;
|
|
419
474
|
}
|
|
420
475
|
//#endregion
|
|
421
|
-
|
|
476
|
+
//#region src/helper/error/exception.d.ts
|
|
477
|
+
type ParserErrorLocation = "reqbody" | "reqparams" | "reqquery";
|
|
478
|
+
/**
|
|
479
|
+
* This error should be thrown when some data cannot be parsed by a given schema.
|
|
480
|
+
*/
|
|
481
|
+
declare class ParserError extends ResponseStatusError {
|
|
482
|
+
constructor(location: ParserErrorLocation, issues: readonly StandardSchemaV1.Issue[], vendor: string);
|
|
483
|
+
private static formatMessage;
|
|
484
|
+
}
|
|
485
|
+
//#endregion
|
|
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
|
+
export { Class, Controller, DELETE, 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,22 @@ 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
|
|
200
216
|
//#region src/helper/sapling.ts
|
|
201
217
|
const settings = {
|
|
202
218
|
serialize: JSON.stringify,
|
|
@@ -270,34 +286,6 @@ var Sapling = class Sapling {
|
|
|
270
286
|
app.use(Sapling.json());
|
|
271
287
|
}
|
|
272
288
|
/**
|
|
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
289
|
* Serialize a value into a JSON string.
|
|
302
290
|
*
|
|
303
291
|
* This function is used in {@link ResponseEntity} to serialize the `body`.
|
|
@@ -335,22 +323,6 @@ var Sapling = class Sapling {
|
|
|
335
323
|
}
|
|
336
324
|
};
|
|
337
325
|
//#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
326
|
//#region src/types.ts
|
|
355
327
|
const methodResolve = {
|
|
356
328
|
GET: "get",
|
|
@@ -483,6 +455,80 @@ function _resolve(ctor) {
|
|
|
483
455
|
return _InjectableRegistry.get(ctor);
|
|
484
456
|
}
|
|
485
457
|
//#endregion
|
|
458
|
+
//#region src/helper/error/exception.ts
|
|
459
|
+
/**
|
|
460
|
+
* This error should be thrown when some data cannot be parsed by a given schema.
|
|
461
|
+
*/
|
|
462
|
+
var ParserError = class ParserError extends ResponseStatusError {
|
|
463
|
+
constructor(location, issues, vendor) {
|
|
464
|
+
super(400, ParserError.formatMessage(location, issues, vendor));
|
|
465
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
466
|
+
}
|
|
467
|
+
static formatMessage(location, issues, vendor) {
|
|
468
|
+
const formatted = issues.map((i) => {
|
|
469
|
+
const path = Array.isArray(i.path) ? i.path.map((seg) => typeof seg === "object" && seg ? String(seg.key) : String(seg)).join(".") : "";
|
|
470
|
+
return path ? `${path}: ${i.message}` : i.message;
|
|
471
|
+
}).join("; ");
|
|
472
|
+
return `${vendor} failed to parse ${(() => {
|
|
473
|
+
switch (location) {
|
|
474
|
+
case "reqbody": return "request body";
|
|
475
|
+
case "reqparams": return "request params";
|
|
476
|
+
case "reqquery": return "request query";
|
|
477
|
+
}
|
|
478
|
+
})()}: ${formatted}`;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
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,4 @@ 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
|
+
export { Controller, DELETE, 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.0",
|
|
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
|
}
|