@tahminator/sapling 2.0.4 → 2.0.5-beta.18ab8bae
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -4
- package/dist/index.cjs +364 -188
- package/dist/index.d.cts +464 -91
- package/dist/index.d.mts +464 -91
- package/dist/index.mjs +336 -188
- package/package.json +8 -2
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import e, { Router } from "express";
|
|
2
|
+
import swagger from "swagger-ui-express";
|
|
2
3
|
//#region src/html/404.ts
|
|
3
4
|
/**
|
|
4
5
|
* Default Express.js 404 error page, as a string.
|
|
@@ -232,15 +233,20 @@ var ParserError = class ParserError extends ResponseStatusError {
|
|
|
232
233
|
case "reqbody": return "request body";
|
|
233
234
|
case "reqparams": return "request params";
|
|
234
235
|
case "reqquery": return "request query";
|
|
236
|
+
case "resbody": return "response body";
|
|
235
237
|
}
|
|
236
238
|
})()}: ${formatted}`;
|
|
237
239
|
}
|
|
238
240
|
};
|
|
239
241
|
//#endregion
|
|
240
242
|
//#region src/helper/sapling.ts
|
|
241
|
-
const
|
|
243
|
+
const _settings = {
|
|
242
244
|
serialize: JSON.stringify,
|
|
243
|
-
deserialize: JSON.parse
|
|
245
|
+
deserialize: JSON.parse,
|
|
246
|
+
doc: {
|
|
247
|
+
openApiPath: "/openapi.json",
|
|
248
|
+
swaggerPath: "/swagger.html"
|
|
249
|
+
}
|
|
244
250
|
};
|
|
245
251
|
/**
|
|
246
252
|
* Collection of utility functions which are essential for Sapling to function.
|
|
@@ -319,13 +325,13 @@ var Sapling = class Sapling {
|
|
|
319
325
|
* @defaultValue `JSON.stringify`
|
|
320
326
|
*/
|
|
321
327
|
static serialize(value) {
|
|
322
|
-
return
|
|
328
|
+
return _settings.serialize(value);
|
|
323
329
|
}
|
|
324
330
|
/**
|
|
325
331
|
* Replace the function used for `serialize`.
|
|
326
332
|
*/
|
|
327
333
|
static setSerializeFn(fn) {
|
|
328
|
-
|
|
334
|
+
_settings.serialize = fn;
|
|
329
335
|
}
|
|
330
336
|
/**
|
|
331
337
|
* De-serialize a JSON string back to a JavaScript object.
|
|
@@ -337,15 +343,264 @@ var Sapling = class Sapling {
|
|
|
337
343
|
* @defaultValue `JSON.parse`
|
|
338
344
|
*/
|
|
339
345
|
static deserialize(value) {
|
|
340
|
-
return
|
|
346
|
+
return _settings.deserialize(value);
|
|
341
347
|
}
|
|
342
348
|
/**
|
|
343
349
|
* Replace the function used for `deserialize`
|
|
344
350
|
*/
|
|
345
351
|
static setDeserializeFn(fn) {
|
|
346
|
-
|
|
352
|
+
_settings.deserialize = fn;
|
|
353
|
+
}
|
|
354
|
+
static setOpenApiPath(path) {
|
|
355
|
+
_settings.doc.openApiPath = path;
|
|
356
|
+
}
|
|
357
|
+
static setSwaggerPath(path) {
|
|
358
|
+
_settings.doc.swaggerPath = path;
|
|
359
|
+
}
|
|
360
|
+
static chainHandlers(handlers, request, response, next, index = 0) {
|
|
361
|
+
if (index >= handlers.length) {
|
|
362
|
+
next();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
handlers[index]?.(request, response, (err) => {
|
|
366
|
+
if (err) {
|
|
367
|
+
next(err);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
Sapling.chainHandlers(handlers, request, response, next, index + 1);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/annotation/route.ts
|
|
376
|
+
const _routeStore = /* @__PURE__ */ new WeakMap();
|
|
377
|
+
/**
|
|
378
|
+
* Custom annotation that will store all routes inside of a map,
|
|
379
|
+
* which can then be used to initialize all the routes to the router.
|
|
380
|
+
*/
|
|
381
|
+
function _Route({ method, path = "" }) {
|
|
382
|
+
return (target, propertyKey) => {
|
|
383
|
+
const ctor = target.constructor;
|
|
384
|
+
const list = _routeStore.get(ctor) ?? [];
|
|
385
|
+
list.push({
|
|
386
|
+
method,
|
|
387
|
+
path: path ?? "",
|
|
388
|
+
fnName: String(propertyKey)
|
|
389
|
+
});
|
|
390
|
+
_routeStore.set(ctor, list);
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Register GET route on the given path (default "") for the given controller.
|
|
395
|
+
*/
|
|
396
|
+
const GET = (path = "") => _Route({
|
|
397
|
+
method: "GET",
|
|
398
|
+
path
|
|
399
|
+
});
|
|
400
|
+
/**
|
|
401
|
+
* Register POST route on the given path (default "") for the given controller.
|
|
402
|
+
*/
|
|
403
|
+
const POST = (path = "") => _Route({
|
|
404
|
+
method: "POST",
|
|
405
|
+
path
|
|
406
|
+
});
|
|
407
|
+
/**
|
|
408
|
+
* Register PUT route on the given path (default "") for the given controller.
|
|
409
|
+
*/
|
|
410
|
+
const PUT = (path = "") => _Route({
|
|
411
|
+
method: "PUT",
|
|
412
|
+
path
|
|
413
|
+
});
|
|
414
|
+
/**
|
|
415
|
+
* Register DELETE route on the given path (default "") for the given controller.
|
|
416
|
+
*/
|
|
417
|
+
const DELETE = (path = "") => _Route({
|
|
418
|
+
method: "DELETE",
|
|
419
|
+
path
|
|
420
|
+
});
|
|
421
|
+
/**
|
|
422
|
+
* Register OPTIONS route on the given path (default "") for the given controller.
|
|
423
|
+
*/
|
|
424
|
+
const OPTIONS = (path = "") => _Route({
|
|
425
|
+
method: "OPTIONS",
|
|
426
|
+
path
|
|
427
|
+
});
|
|
428
|
+
/**
|
|
429
|
+
* Register PATCH route on the given path (default "") for the given controller.
|
|
430
|
+
*/
|
|
431
|
+
const PATCH = (path = "") => _Route({
|
|
432
|
+
method: "PATCH",
|
|
433
|
+
path
|
|
434
|
+
});
|
|
435
|
+
/**
|
|
436
|
+
* Register HEAD route on the given path (default "") for the given controller.
|
|
437
|
+
*/
|
|
438
|
+
const HEAD = (path = "") => _Route({
|
|
439
|
+
method: "HEAD",
|
|
440
|
+
path
|
|
441
|
+
});
|
|
442
|
+
/**
|
|
443
|
+
* Register a middleware route on the given path (default "") for the given controller.
|
|
444
|
+
*/
|
|
445
|
+
const Middleware = (path = "") => _Route({
|
|
446
|
+
method: "USE",
|
|
447
|
+
path
|
|
448
|
+
});
|
|
449
|
+
/**
|
|
450
|
+
* Given a class constructor, fetch all the routes attached.
|
|
451
|
+
*/
|
|
452
|
+
function _getRoutes(ctor) {
|
|
453
|
+
return _routeStore.get(ctor) ?? [];
|
|
454
|
+
}
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/utils.ts
|
|
457
|
+
function _getOrCreateMap(store, ctor) {
|
|
458
|
+
const existing = store.get(ctor);
|
|
459
|
+
if (existing) return existing;
|
|
460
|
+
const created = /* @__PURE__ */ new Map();
|
|
461
|
+
store.set(ctor, created);
|
|
462
|
+
return created;
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/annotation/schema.ts
|
|
466
|
+
const _routeSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
467
|
+
const _controllerSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
468
|
+
function ControllerSchema(options) {
|
|
469
|
+
return (target) => {
|
|
470
|
+
_setControllerSchema(target, options);
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function RouteSchema(options) {
|
|
474
|
+
return (target, propertyKey) => {
|
|
475
|
+
const ctor = target.constructor;
|
|
476
|
+
_setRouteSchema(ctor, String(propertyKey), options);
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function _setRouteSchema(ctor, fnName, options) {
|
|
480
|
+
_getOrCreateMap(_routeSchemaStore, ctor).set(fnName, options);
|
|
481
|
+
}
|
|
482
|
+
function _setControllerSchema(ctor, options) {
|
|
483
|
+
_controllerSchemaStore.set(ctor, options);
|
|
484
|
+
}
|
|
485
|
+
function _getRouteSchema(ctor, fnName) {
|
|
486
|
+
return _routeSchemaStore.get(ctor)?.get(fnName);
|
|
487
|
+
}
|
|
488
|
+
function _getControllerSchema(ctor) {
|
|
489
|
+
return _controllerSchemaStore.get(ctor);
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/helper/openapi.ts
|
|
493
|
+
var OpenAPIGenerator = class {
|
|
494
|
+
constructor() {
|
|
495
|
+
this.controllers = /* @__PURE__ */ new Set();
|
|
496
|
+
this.config = {
|
|
497
|
+
title: "API",
|
|
498
|
+
version: "1.0.0"
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
setConfig(config) {
|
|
502
|
+
this.config = config;
|
|
503
|
+
}
|
|
504
|
+
registerController(controllerClass, prefix) {
|
|
505
|
+
this.controllers.add({
|
|
506
|
+
class: controllerClass,
|
|
507
|
+
prefix
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
generateSpec() {
|
|
511
|
+
const config = this.config;
|
|
512
|
+
const paths = {};
|
|
513
|
+
const tags = [];
|
|
514
|
+
for (const { class: controllerClass, prefix } of this.controllers) {
|
|
515
|
+
const routes = _getRoutes(controllerClass);
|
|
516
|
+
const controllerSchema = _getControllerSchema(controllerClass);
|
|
517
|
+
if (controllerSchema?.title) tags.push({
|
|
518
|
+
name: controllerSchema.title,
|
|
519
|
+
description: controllerSchema.description
|
|
520
|
+
});
|
|
521
|
+
for (const route of routes) {
|
|
522
|
+
if (route.method === "USE") continue;
|
|
523
|
+
const schemas = _getValidatorSchema(controllerClass, route.fnName);
|
|
524
|
+
const routeSchema = _getRouteSchema(controllerClass, route.fnName);
|
|
525
|
+
if (route.path instanceof RegExp) throw new Error(`You have a route with a regex path of ${route.path.source}. This is not compatible with OpenAPI.`);
|
|
526
|
+
const openApiPath = prefix + route.path;
|
|
527
|
+
if (!paths[openApiPath]) paths[openApiPath] = {};
|
|
528
|
+
const responses = {};
|
|
529
|
+
if (schemas?.responseBody) responses["200"] = {
|
|
530
|
+
description: "Successful response",
|
|
531
|
+
content: { "application/json": { schema: this.toJsonSchema(schemas.responseBody) } }
|
|
532
|
+
};
|
|
533
|
+
else responses["200"] = { description: "Successful response" };
|
|
534
|
+
if (routeSchema?.responses) for (const resp of routeSchema.responses) responses[String(resp.statusCode)] = {
|
|
535
|
+
description: `Response ${resp.statusCode}`,
|
|
536
|
+
content: { "application/json": { schema: this.toJsonSchema(resp.schema) } }
|
|
537
|
+
};
|
|
538
|
+
const operation = {
|
|
539
|
+
responses,
|
|
540
|
+
description: routeSchema?.description,
|
|
541
|
+
tags: controllerSchema?.title ? [controllerSchema.title] : void 0
|
|
542
|
+
};
|
|
543
|
+
const parameters = [];
|
|
544
|
+
if (schemas?.requestParam) {
|
|
545
|
+
const paramSchema = this.toJsonSchema(schemas.requestParam);
|
|
546
|
+
if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) parameters.push({
|
|
547
|
+
name,
|
|
548
|
+
in: "path",
|
|
549
|
+
required: true,
|
|
550
|
+
schema
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
if (schemas?.requestQuery) {
|
|
554
|
+
const querySchema = this.toJsonSchema(schemas.requestQuery);
|
|
555
|
+
if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
|
|
556
|
+
const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
|
|
557
|
+
parameters.push({
|
|
558
|
+
name,
|
|
559
|
+
in: "query",
|
|
560
|
+
required: isRequired,
|
|
561
|
+
schema
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (parameters.length > 0) operation.parameters = parameters;
|
|
566
|
+
if (schemas?.requestBody) operation.requestBody = {
|
|
567
|
+
required: true,
|
|
568
|
+
content: { "application/json": { schema: this.toJsonSchema(schemas.requestBody) } }
|
|
569
|
+
};
|
|
570
|
+
const method = route.method.toLowerCase();
|
|
571
|
+
paths[openApiPath][method] = operation;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
openapi: "3.0.0",
|
|
576
|
+
info: {
|
|
577
|
+
title: config.title,
|
|
578
|
+
version: config.version,
|
|
579
|
+
description: config.description
|
|
580
|
+
},
|
|
581
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
582
|
+
paths
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
toJsonSchema(schema) {
|
|
586
|
+
try {
|
|
587
|
+
return schema["~standard"].jsonSchema.output({ target: "openapi-3.0" });
|
|
588
|
+
} catch (e) {
|
|
589
|
+
if (e instanceof Error && e.message.includes("Transforms cannot be represented in JSON Schema")) throw new Error(`${e.message}.\nIt appears that you are using z.transform() - it is highly recommended that you use z.codec instead - https://zod.dev/codecs`);
|
|
590
|
+
throw e;
|
|
591
|
+
}
|
|
347
592
|
}
|
|
348
593
|
};
|
|
594
|
+
const openApiGenerator = new OpenAPIGenerator();
|
|
595
|
+
function _registerControllerClass(controllerClass, prefix) {
|
|
596
|
+
openApiGenerator.registerController(controllerClass, prefix);
|
|
597
|
+
}
|
|
598
|
+
function setOpenApiConfig(config) {
|
|
599
|
+
openApiGenerator.setConfig(config);
|
|
600
|
+
}
|
|
601
|
+
function generateOpenApiSpec() {
|
|
602
|
+
return openApiGenerator.generateSpec();
|
|
603
|
+
}
|
|
349
604
|
//#endregion
|
|
350
605
|
//#region src/types.ts
|
|
351
606
|
const methodResolve = {
|
|
@@ -479,124 +734,44 @@ function _resolve(ctor) {
|
|
|
479
734
|
return _InjectableRegistry.get(ctor);
|
|
480
735
|
}
|
|
481
736
|
//#endregion
|
|
482
|
-
//#region src/annotation/
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
* ```ts
|
|
492
|
-
* const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
|
|
493
|
-
* name: z.string(),
|
|
494
|
-
* description: z.string().optional(),
|
|
495
|
-
* });
|
|
496
|
-
*
|
|
497
|
-
* ⠀@Controller({ prefix: "/api/book" })
|
|
498
|
-
* class BookController {
|
|
499
|
-
* ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
|
|
500
|
-
* ⠀@POST()
|
|
501
|
-
* public createBook(request: e.Request) {
|
|
502
|
-
* const { name, description } = request.body as unknown as z.infer<
|
|
503
|
-
* typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
|
|
504
|
-
* >;
|
|
505
|
-
* }
|
|
506
|
-
* }
|
|
507
|
-
* ```
|
|
508
|
-
*/
|
|
737
|
+
//#region src/annotation/validator.ts
|
|
738
|
+
const _validatorSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
739
|
+
function ResponseBody(schema) {
|
|
740
|
+
return (target, propertyKey) => {
|
|
741
|
+
const ctor = target.constructor;
|
|
742
|
+
const fnName = String(propertyKey);
|
|
743
|
+
_setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "responseBody", schema, fnName);
|
|
744
|
+
};
|
|
745
|
+
}
|
|
509
746
|
function RequestBody(schema) {
|
|
510
747
|
return (target, propertyKey) => {
|
|
511
748
|
const ctor = target.constructor;
|
|
512
749
|
const fnName = String(propertyKey);
|
|
513
|
-
_setOnce(
|
|
750
|
+
_setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestBody", schema, fnName);
|
|
514
751
|
};
|
|
515
752
|
}
|
|
516
|
-
/**
|
|
517
|
-
* Apply to a route method to have `request.param` be parsed by `schema`.
|
|
518
|
-
*
|
|
519
|
-
* This annotation will parse `request.param` & then override `request.param`.
|
|
520
|
-
* You can then just simply cast `request.param` for your use
|
|
521
|
-
*
|
|
522
|
-
* @example
|
|
523
|
-
* ```ts
|
|
524
|
-
* const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
|
|
525
|
-
* bookId: z.string(),
|
|
526
|
-
* });
|
|
527
|
-
*
|
|
528
|
-
* ⠀@Controller({ prefix: "/api/book" })
|
|
529
|
-
* class BookController {
|
|
530
|
-
* ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
|
|
531
|
-
* ⠀@GET("/:bookId")
|
|
532
|
-
* public getBook(request: e.Request) {
|
|
533
|
-
* const { bookId } = request.param as unknown as z.infer<
|
|
534
|
-
* typeof GET_BOOK_REQUEST_PARAM_SCHEMA
|
|
535
|
-
* >;
|
|
536
|
-
* }
|
|
537
|
-
* }
|
|
538
|
-
* ```
|
|
539
|
-
*/
|
|
540
753
|
function RequestParam(schema) {
|
|
541
754
|
return (target, propertyKey) => {
|
|
542
755
|
const ctor = target.constructor;
|
|
543
756
|
const fnName = String(propertyKey);
|
|
544
|
-
_setOnce(
|
|
757
|
+
_setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestParam", schema, fnName);
|
|
545
758
|
};
|
|
546
759
|
}
|
|
547
|
-
/**
|
|
548
|
-
* Apply to a route method to have `request.query` be parsed by `schema`.
|
|
549
|
-
*
|
|
550
|
-
* This annotation will parse `request.query` & then override `request.query`.
|
|
551
|
-
* You can then just simply cast `request.query` for your use
|
|
552
|
-
*
|
|
553
|
-
* @example
|
|
554
|
-
* ```ts
|
|
555
|
-
* const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
|
|
556
|
-
* sort: z.enum(["name", "createdAt"]).optional(),
|
|
557
|
-
* q: z.string().optional(),
|
|
558
|
-
* });
|
|
559
|
-
*
|
|
560
|
-
* ⠀@Controller({ prefix: "/api/book" })
|
|
561
|
-
* class BookController {
|
|
562
|
-
* ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
|
|
563
|
-
* ⠀@GET()
|
|
564
|
-
* public listBooks(request: e.Request) {
|
|
565
|
-
* const { sort, q } = request.query as unknown as z.infer<
|
|
566
|
-
* typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
|
|
567
|
-
* >;
|
|
568
|
-
* }
|
|
569
|
-
* }
|
|
570
|
-
* ```
|
|
571
|
-
*/
|
|
572
760
|
function RequestQuery(schema) {
|
|
573
761
|
return (target, propertyKey) => {
|
|
574
762
|
const ctor = target.constructor;
|
|
575
763
|
const fnName = String(propertyKey);
|
|
576
|
-
_setOnce(
|
|
764
|
+
_setOnce(_getOrCreateSchemaDefinition(ctor, fnName), "requestQuery", schema, fnName);
|
|
577
765
|
};
|
|
578
766
|
}
|
|
579
|
-
function
|
|
580
|
-
const byFn = (
|
|
581
|
-
const fn = _requestSchemaStore.get(ctor);
|
|
582
|
-
if (fn) return fn;
|
|
583
|
-
const newFn = /* @__PURE__ */ new Map();
|
|
584
|
-
_requestSchemaStore.set(ctor, newFn);
|
|
585
|
-
return newFn;
|
|
586
|
-
})();
|
|
767
|
+
function _getOrCreateSchemaDefinition(ctor, fnName) {
|
|
768
|
+
const byFn = _getOrCreateMap(_validatorSchemaStore, ctor);
|
|
587
769
|
const existing = byFn.get(fnName);
|
|
588
770
|
if (existing) return existing;
|
|
589
771
|
const created = {};
|
|
590
772
|
byFn.set(fnName, created);
|
|
591
773
|
return created;
|
|
592
774
|
}
|
|
593
|
-
function _setOnce(def, key, schema, fnName) {
|
|
594
|
-
if (def[key]) throw new Error(`Duplicate request schema for "${String(key)}" on method "${fnName}"`);
|
|
595
|
-
def[key] = schema;
|
|
596
|
-
}
|
|
597
|
-
function _getRequestSchemas(ctor, fnName) {
|
|
598
|
-
return _requestSchemaStore.get(ctor)?.get(fnName);
|
|
599
|
-
}
|
|
600
775
|
async function _parseOrThrow(schema, input, kind) {
|
|
601
776
|
const result = await schema["~standard"].validate(input);
|
|
602
777
|
if (result.issues) {
|
|
@@ -605,86 +780,12 @@ async function _parseOrThrow(schema, input, kind) {
|
|
|
605
780
|
}
|
|
606
781
|
return result.value;
|
|
607
782
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const _routeStore = /* @__PURE__ */ new WeakMap();
|
|
611
|
-
/**
|
|
612
|
-
* Custom annotation that will store all routes inside of a map,
|
|
613
|
-
* which can then be used to initialize all the routes to the router.
|
|
614
|
-
*/
|
|
615
|
-
function _Route({ method, path = "" }) {
|
|
616
|
-
return (target, propertyKey) => {
|
|
617
|
-
const ctor = target.constructor;
|
|
618
|
-
const list = _routeStore.get(ctor) ?? [];
|
|
619
|
-
list.push({
|
|
620
|
-
method,
|
|
621
|
-
path: path ?? "",
|
|
622
|
-
fnName: String(propertyKey)
|
|
623
|
-
});
|
|
624
|
-
_routeStore.set(ctor, list);
|
|
625
|
-
};
|
|
783
|
+
function _getValidatorSchema(ctor, fnName) {
|
|
784
|
+
return _validatorSchemaStore.get(ctor)?.get(fnName);
|
|
626
785
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const GET = (path = "") => _Route({
|
|
631
|
-
method: "GET",
|
|
632
|
-
path
|
|
633
|
-
});
|
|
634
|
-
/**
|
|
635
|
-
* Register POST route on the given path (default "") for the given controller.
|
|
636
|
-
*/
|
|
637
|
-
const POST = (path = "") => _Route({
|
|
638
|
-
method: "POST",
|
|
639
|
-
path
|
|
640
|
-
});
|
|
641
|
-
/**
|
|
642
|
-
* Register PUT route on the given path (default "") for the given controller.
|
|
643
|
-
*/
|
|
644
|
-
const PUT = (path = "") => _Route({
|
|
645
|
-
method: "PUT",
|
|
646
|
-
path
|
|
647
|
-
});
|
|
648
|
-
/**
|
|
649
|
-
* Register DELETE route on the given path (default "") for the given controller.
|
|
650
|
-
*/
|
|
651
|
-
const DELETE = (path = "") => _Route({
|
|
652
|
-
method: "DELETE",
|
|
653
|
-
path
|
|
654
|
-
});
|
|
655
|
-
/**
|
|
656
|
-
* Register OPTIONS route on the given path (default "") for the given controller.
|
|
657
|
-
*/
|
|
658
|
-
const OPTIONS = (path = "") => _Route({
|
|
659
|
-
method: "OPTIONS",
|
|
660
|
-
path
|
|
661
|
-
});
|
|
662
|
-
/**
|
|
663
|
-
* Register PATCH route on the given path (default "") for the given controller.
|
|
664
|
-
*/
|
|
665
|
-
const PATCH = (path = "") => _Route({
|
|
666
|
-
method: "PATCH",
|
|
667
|
-
path
|
|
668
|
-
});
|
|
669
|
-
/**
|
|
670
|
-
* Register HEAD route on the given path (default "") for the given controller.
|
|
671
|
-
*/
|
|
672
|
-
const HEAD = (path = "") => _Route({
|
|
673
|
-
method: "HEAD",
|
|
674
|
-
path
|
|
675
|
-
});
|
|
676
|
-
/**
|
|
677
|
-
* Register a middleware route on the given path (default "") for the given controller.
|
|
678
|
-
*/
|
|
679
|
-
const Middleware = (path = "") => _Route({
|
|
680
|
-
method: "USE",
|
|
681
|
-
path
|
|
682
|
-
});
|
|
683
|
-
/**
|
|
684
|
-
* Given a class constructor, fetch all the routes attached.
|
|
685
|
-
*/
|
|
686
|
-
function _getRoutes(ctor) {
|
|
687
|
-
return _routeStore.get(ctor) ?? [];
|
|
786
|
+
function _setOnce(def, key, schema, fnName) {
|
|
787
|
+
if (def[key]) throw new Error(`Duplicate schema for "${String(key)}" on method "${fnName}"`);
|
|
788
|
+
def[key] = schema;
|
|
688
789
|
}
|
|
689
790
|
//#endregion
|
|
690
791
|
//#region src/annotation/controller.ts
|
|
@@ -698,6 +799,7 @@ const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
|
|
|
698
799
|
function Controller({ prefix = "", deps = [] } = {}) {
|
|
699
800
|
return (target) => {
|
|
700
801
|
const targetClass = target;
|
|
802
|
+
_registerControllerClass(target, prefix);
|
|
701
803
|
const router = Router();
|
|
702
804
|
const routes = _getRoutes(target);
|
|
703
805
|
const usedRoutes = /* @__PURE__ */ new Set();
|
|
@@ -740,12 +842,12 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
|
|
|
740
842
|
return;
|
|
741
843
|
}
|
|
742
844
|
router[methodName](fp, async (request, response, next) => {
|
|
743
|
-
const schemas =
|
|
845
|
+
const schemas = _getValidatorSchema(target, fnName);
|
|
744
846
|
if (schemas) {
|
|
745
|
-
if (schemas.
|
|
746
|
-
if (schemas.
|
|
747
|
-
if (schemas.
|
|
748
|
-
const parsedQuery = await _parseOrThrow(schemas.
|
|
847
|
+
if (schemas.requestBody) request.body = await _parseOrThrow(schemas.requestBody, request.body, "reqbody");
|
|
848
|
+
if (schemas.requestParam) request.params = await _parseOrThrow(schemas.requestParam, request.params, "reqparams");
|
|
849
|
+
if (schemas.requestQuery) {
|
|
850
|
+
const parsedQuery = await _parseOrThrow(schemas.requestQuery, request.query, "reqquery");
|
|
749
851
|
Object.defineProperty(request, "query", {
|
|
750
852
|
value: parsedQuery,
|
|
751
853
|
writable: true,
|
|
@@ -755,7 +857,8 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
|
|
|
755
857
|
}
|
|
756
858
|
const result = await fn.bind(controllerInstance)(request, response, next);
|
|
757
859
|
if (result instanceof ResponseEntity) {
|
|
758
|
-
|
|
860
|
+
const body = schemas && schemas.responseBody ? await _parseOrThrow(schemas.responseBody, result.getBody(), "resbody") : result.getBody();
|
|
861
|
+
response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(body));
|
|
759
862
|
return;
|
|
760
863
|
}
|
|
761
864
|
if (result instanceof RedirectView) {
|
|
@@ -789,7 +892,7 @@ function __decorate(decorators, target, key, desc) {
|
|
|
789
892
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
790
893
|
}
|
|
791
894
|
//#endregion
|
|
792
|
-
//#region src/middleware/default/base.ts
|
|
895
|
+
//#region src/middleware/default/error/base.ts
|
|
793
896
|
let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
|
|
794
897
|
handle(err, _request, _response, _next) {
|
|
795
898
|
console.error("[Error]", err);
|
|
@@ -799,7 +902,17 @@ let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
|
|
|
799
902
|
__decorate([Middleware()], DefaultBaseErrorMiddleware.prototype, "handle", null);
|
|
800
903
|
DefaultBaseErrorMiddleware = __decorate([MiddlewareClass()], DefaultBaseErrorMiddleware);
|
|
801
904
|
//#endregion
|
|
802
|
-
//#region src/middleware/default/
|
|
905
|
+
//#region src/middleware/default/error/parse.ts
|
|
906
|
+
let DefaultParserErrorMiddleware = class DefaultParserErrorMiddleware {
|
|
907
|
+
handle(err, _request, _response, next) {
|
|
908
|
+
if (err instanceof ParserError) return ResponseEntity.status(err.status).body({ message: err.message });
|
|
909
|
+
next(err);
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
__decorate([Middleware()], DefaultParserErrorMiddleware.prototype, "handle", null);
|
|
913
|
+
DefaultParserErrorMiddleware = __decorate([MiddlewareClass()], DefaultParserErrorMiddleware);
|
|
914
|
+
//#endregion
|
|
915
|
+
//#region src/middleware/default/error/responsestatus.ts
|
|
803
916
|
let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddleware {
|
|
804
917
|
handle(err, _request, _response, next) {
|
|
805
918
|
if (err instanceof ResponseStatusError) return ResponseEntity.status(err.status).body({ message: err.message });
|
|
@@ -809,4 +922,39 @@ let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddl
|
|
|
809
922
|
__decorate([Middleware()], DefaultResponseStatusErrorMiddleware.prototype, "handle", null);
|
|
810
923
|
DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultResponseStatusErrorMiddleware);
|
|
811
924
|
//#endregion
|
|
812
|
-
|
|
925
|
+
//#region src/middleware/default/openapi/index.ts
|
|
926
|
+
let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
|
|
927
|
+
handle(_request, _response, _next) {
|
|
928
|
+
return ResponseEntity.ok().body(generateOpenApiSpec());
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
__decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
|
|
932
|
+
DefaultOpenApiMiddleware = __decorate([MiddlewareClass()], DefaultOpenApiMiddleware);
|
|
933
|
+
//#endregion
|
|
934
|
+
//#region src/middleware/default/swagger/index.ts
|
|
935
|
+
let Serve = class Serve {
|
|
936
|
+
constructor() {
|
|
937
|
+
this.handlers = swagger.serve;
|
|
938
|
+
}
|
|
939
|
+
handle(request, response, next) {
|
|
940
|
+
return Sapling.chainHandlers(this.handlers, request, response, next);
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
__decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
|
|
944
|
+
Serve = __decorate([MiddlewareClass()], Serve);
|
|
945
|
+
let Setup = class Setup {
|
|
946
|
+
constructor() {
|
|
947
|
+
this.handler = swagger.setup(null, { swaggerOptions: { url: _settings.doc.openApiPath } });
|
|
948
|
+
}
|
|
949
|
+
handle(request, response, next) {
|
|
950
|
+
return this.handler(request, response, next);
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
__decorate([Middleware(_settings.doc.swaggerPath)], Setup.prototype, "handle", null);
|
|
954
|
+
Setup = __decorate([MiddlewareClass()], Setup);
|
|
955
|
+
const DefaultSwaggerMiddleware = {
|
|
956
|
+
Serve,
|
|
957
|
+
Setup
|
|
958
|
+
};
|
|
959
|
+
//#endregion
|
|
960
|
+
export { Controller, ControllerSchema, DELETE, DefaultBaseErrorMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, GET, HEAD, Html404ErrorPage, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseBody, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, RouteSchema, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getControllerSchema, _getOrCreateSchemaDefinition, _getRouteSchema, _getRoutes, _getValidatorSchema, _parseOrThrow, _registerControllerClass, _resolve, _setControllerSchema, _setOnce, _setRouteSchema, _settings, generateOpenApiSpec, methodResolve, openApiGenerator, setOpenApiConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tahminator/sapling",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5-beta.18ab8bae",
|
|
4
4
|
"author": "Tahmid Ahmed",
|
|
5
5
|
"description": "A library to help you write cleaner Express.js code",
|
|
6
6
|
"repository": {
|
|
@@ -44,12 +44,14 @@
|
|
|
44
44
|
"@standard-schema/spec": "^1.1.0",
|
|
45
45
|
"@types/express": "^5",
|
|
46
46
|
"@types/supertest": "^7.2.0",
|
|
47
|
+
"@types/swagger-ui-express": "^4.1.8",
|
|
47
48
|
"@vitest/coverage-istanbul": "^4.1.2",
|
|
48
49
|
"eslint": "^10.1.0",
|
|
49
50
|
"eslint-plugin-perfectionist": "^5.7.0",
|
|
50
51
|
"globals": "^17.4.0",
|
|
51
52
|
"jiti": "^2.6.1",
|
|
52
53
|
"jsdom": "^29.0.1",
|
|
54
|
+
"openapi-types": "12.1.3",
|
|
53
55
|
"prettier": "^3.8.1",
|
|
54
56
|
"superjson": "^2.2.6",
|
|
55
57
|
"supertest": "^7.2.2",
|
|
@@ -60,6 +62,10 @@
|
|
|
60
62
|
"zod": "^4.4.3"
|
|
61
63
|
},
|
|
62
64
|
"inlinedDependencies": {
|
|
63
|
-
"@standard-schema/spec": "1.1.0"
|
|
65
|
+
"@standard-schema/spec": "1.1.0",
|
|
66
|
+
"openapi-types": "12.1.3"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"swagger-ui-express": "^5.0.1"
|
|
64
70
|
}
|
|
65
71
|
}
|