@tahminator/sapling 2.0.3 → 2.0.5-beta.2f539758

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -240,7 +240,8 @@ var ParserError = class ParserError extends ResponseStatusError {
240
240
  //#region src/helper/sapling.ts
241
241
  const settings = {
242
242
  serialize: JSON.stringify,
243
- deserialize: JSON.parse
243
+ deserialize: JSON.parse,
244
+ openapi: { path: "/openapi.json" }
244
245
  };
245
246
  /**
246
247
  * Collection of utility functions which are essential for Sapling to function.
@@ -345,7 +346,304 @@ var Sapling = class Sapling {
345
346
  static setDeserializeFn(fn) {
346
347
  settings.deserialize = fn;
347
348
  }
349
+ static setOpenApiPath(path) {
350
+ settings.openapi.path = path;
351
+ }
352
+ };
353
+ //#endregion
354
+ //#region src/annotation/request.ts
355
+ const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
356
+ /**
357
+ * Apply to a route method to have `request.body` be parsed by `schema`.
358
+ *
359
+ * This annotation will parse `request.body` & then override `request.body`.
360
+ * You can then just simply cast `request.body` for your use
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
365
+ * name: z.string(),
366
+ * description: z.string().optional(),
367
+ * });
368
+ *
369
+ * ⠀@Controller({ prefix: "/api/book" })
370
+ * class BookController {
371
+ * ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
372
+ * ⠀@POST()
373
+ * public createBook(request: e.Request) {
374
+ * const { name, description } = request.body as unknown as z.infer<
375
+ * typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
376
+ * >;
377
+ * }
378
+ * }
379
+ * ```
380
+ */
381
+ function RequestBody(schema) {
382
+ return (target, propertyKey) => {
383
+ const ctor = target.constructor;
384
+ const fnName = String(propertyKey);
385
+ _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "body", schema, fnName);
386
+ };
387
+ }
388
+ /**
389
+ * Apply to a route method to have `request.param` be parsed by `schema`.
390
+ *
391
+ * This annotation will parse `request.param` & then override `request.param`.
392
+ * You can then just simply cast `request.param` for your use
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
397
+ * bookId: z.string(),
398
+ * });
399
+ *
400
+ * ⠀@Controller({ prefix: "/api/book" })
401
+ * class BookController {
402
+ * ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
403
+ * ⠀@GET("/:bookId")
404
+ * public getBook(request: e.Request) {
405
+ * const { bookId } = request.param as unknown as z.infer<
406
+ * typeof GET_BOOK_REQUEST_PARAM_SCHEMA
407
+ * >;
408
+ * }
409
+ * }
410
+ * ```
411
+ */
412
+ function RequestParam(schema) {
413
+ return (target, propertyKey) => {
414
+ const ctor = target.constructor;
415
+ const fnName = String(propertyKey);
416
+ _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "param", schema, fnName);
417
+ };
418
+ }
419
+ /**
420
+ * Apply to a route method to have `request.query` be parsed by `schema`.
421
+ *
422
+ * This annotation will parse `request.query` & then override `request.query`.
423
+ * You can then just simply cast `request.query` for your use
424
+ *
425
+ * @example
426
+ * ```ts
427
+ * const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
428
+ * sort: z.enum(["name", "createdAt"]).optional(),
429
+ * q: z.string().optional(),
430
+ * });
431
+ *
432
+ * ⠀@Controller({ prefix: "/api/book" })
433
+ * class BookController {
434
+ * ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
435
+ * ⠀@GET()
436
+ * public listBooks(request: e.Request) {
437
+ * const { sort, q } = request.query as unknown as z.infer<
438
+ * typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
439
+ * >;
440
+ * }
441
+ * }
442
+ * ```
443
+ */
444
+ function RequestQuery(schema) {
445
+ return (target, propertyKey) => {
446
+ const ctor = target.constructor;
447
+ const fnName = String(propertyKey);
448
+ _setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "query", schema, fnName);
449
+ };
450
+ }
451
+ function _getOrCreateRequestSchemaDefinition(ctor, fnName) {
452
+ const byFn = (() => {
453
+ const fn = _requestSchemaStore.get(ctor);
454
+ if (fn) return fn;
455
+ const newFn = /* @__PURE__ */ new Map();
456
+ _requestSchemaStore.set(ctor, newFn);
457
+ return newFn;
458
+ })();
459
+ const existing = byFn.get(fnName);
460
+ if (existing) return existing;
461
+ const created = {};
462
+ byFn.set(fnName, created);
463
+ return created;
464
+ }
465
+ function _setOnce(def, key, schema, fnName) {
466
+ if (def[key]) throw new Error(`Duplicate request schema for "${String(key)}" on method "${fnName}"`);
467
+ def[key] = schema;
468
+ }
469
+ function _getRequestSchemas(ctor, fnName) {
470
+ return _requestSchemaStore.get(ctor)?.get(fnName);
471
+ }
472
+ async function _parseOrThrow(schema, input, kind) {
473
+ const result = await schema["~standard"].validate(input);
474
+ if (result.issues) {
475
+ console.debug(`Failed to parse a schema`);
476
+ throw new ParserError(kind, result.issues, schema["~standard"].vendor);
477
+ }
478
+ return result.value;
479
+ }
480
+ //#endregion
481
+ //#region src/annotation/route.ts
482
+ const _routeStore = /* @__PURE__ */ new WeakMap();
483
+ /**
484
+ * Custom annotation that will store all routes inside of a map,
485
+ * which can then be used to initialize all the routes to the router.
486
+ */
487
+ function _Route({ method, path = "" }) {
488
+ return (target, propertyKey) => {
489
+ const ctor = target.constructor;
490
+ const list = _routeStore.get(ctor) ?? [];
491
+ list.push({
492
+ method,
493
+ path: path ?? "",
494
+ fnName: String(propertyKey)
495
+ });
496
+ _routeStore.set(ctor, list);
497
+ };
498
+ }
499
+ /**
500
+ * Register GET route on the given path (default "") for the given controller.
501
+ */
502
+ const GET = (path = "") => _Route({
503
+ method: "GET",
504
+ path
505
+ });
506
+ /**
507
+ * Register POST route on the given path (default "") for the given controller.
508
+ */
509
+ const POST = (path = "") => _Route({
510
+ method: "POST",
511
+ path
512
+ });
513
+ /**
514
+ * Register PUT route on the given path (default "") for the given controller.
515
+ */
516
+ const PUT = (path = "") => _Route({
517
+ method: "PUT",
518
+ path
519
+ });
520
+ /**
521
+ * Register DELETE route on the given path (default "") for the given controller.
522
+ */
523
+ const DELETE = (path = "") => _Route({
524
+ method: "DELETE",
525
+ path
526
+ });
527
+ /**
528
+ * Register OPTIONS route on the given path (default "") for the given controller.
529
+ */
530
+ const OPTIONS = (path = "") => _Route({
531
+ method: "OPTIONS",
532
+ path
533
+ });
534
+ /**
535
+ * Register PATCH route on the given path (default "") for the given controller.
536
+ */
537
+ const PATCH = (path = "") => _Route({
538
+ method: "PATCH",
539
+ path
540
+ });
541
+ /**
542
+ * Register HEAD route on the given path (default "") for the given controller.
543
+ */
544
+ const HEAD = (path = "") => _Route({
545
+ method: "HEAD",
546
+ path
547
+ });
548
+ /**
549
+ * Register a middleware route on the given path (default "") for the given controller.
550
+ */
551
+ const Middleware = (path = "") => _Route({
552
+ method: "USE",
553
+ path
554
+ });
555
+ /**
556
+ * Given a class constructor, fetch all the routes attached.
557
+ */
558
+ function _getRoutes(ctor) {
559
+ return _routeStore.get(ctor) ?? [];
560
+ }
561
+ //#endregion
562
+ //#region src/helper/openapi.ts
563
+ var OpenAPIGenerator = class {
564
+ constructor() {
565
+ this.controllers = /* @__PURE__ */ new Set();
566
+ this.config = {
567
+ title: "API",
568
+ version: "1.0.0"
569
+ };
570
+ }
571
+ setConfig(config) {
572
+ this.config = config;
573
+ }
574
+ registerController(controllerClass, prefix) {
575
+ this.controllers.add({
576
+ class: controllerClass,
577
+ prefix
578
+ });
579
+ }
580
+ generateSpec() {
581
+ const config = this.config;
582
+ const paths = {};
583
+ for (const { class: controllerClass, prefix } of this.controllers) {
584
+ const routes = _getRoutes(controllerClass);
585
+ for (const route of routes) {
586
+ if (route.method === "USE") continue;
587
+ const schemas = _getRequestSchemas(controllerClass, route.fnName);
588
+ const fullPath = route.path instanceof RegExp ? route.path.source : prefix + route.path;
589
+ const openApiPath = typeof fullPath === "string" ? fullPath.replace(/:(\w+)/g, "{$1}") : fullPath;
590
+ if (!paths[openApiPath]) paths[openApiPath] = {};
591
+ const operation = { responses: { "200": { description: "Successful response" } } };
592
+ const parameters = [];
593
+ if (schemas?.param) {
594
+ const paramSchema = this.toJsonSchema(schemas.param);
595
+ if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) parameters.push({
596
+ name,
597
+ in: "path",
598
+ required: true,
599
+ schema
600
+ });
601
+ }
602
+ if (schemas?.query) {
603
+ const querySchema = this.toJsonSchema(schemas.query);
604
+ if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
605
+ const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
606
+ parameters.push({
607
+ name,
608
+ in: "query",
609
+ required: isRequired,
610
+ schema
611
+ });
612
+ }
613
+ }
614
+ if (parameters.length > 0) operation.parameters = parameters;
615
+ if (schemas?.body) operation.requestBody = {
616
+ required: true,
617
+ content: { "application/json": { schema: this.toJsonSchema(schemas.body) } }
618
+ };
619
+ const method = route.method.toLowerCase();
620
+ paths[openApiPath][method] = operation;
621
+ }
622
+ }
623
+ return {
624
+ openapi: "3.0.0",
625
+ info: {
626
+ title: config.title,
627
+ version: config.version,
628
+ description: config.description
629
+ },
630
+ paths
631
+ };
632
+ }
633
+ toJsonSchema(schema) {
634
+ return schema["~standard"].jsonSchema.output({ target: "openapi-3.0" });
635
+ }
348
636
  };
637
+ const openApiGenerator = new OpenAPIGenerator();
638
+ function _registerControllerClass(controllerClass, prefix) {
639
+ openApiGenerator.registerController(controllerClass, prefix);
640
+ }
641
+ function setOpenApiConfig(config) {
642
+ openApiGenerator.setConfig(config);
643
+ }
644
+ function generateOpenApiSpec() {
645
+ return openApiGenerator.generateSpec();
646
+ }
349
647
  //#endregion
350
648
  //#region src/types.ts
351
649
  const methodResolve = {
@@ -479,140 +777,6 @@ function _resolve(ctor) {
479
777
  return _InjectableRegistry.get(ctor);
480
778
  }
481
779
  //#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) {
529
- console.debug(`Failed to parse a schema`);
530
- throw new ParserError(kind, result.issues, schema["~standard"].vendor);
531
- }
532
- return result.value;
533
- }
534
- //#endregion
535
- //#region src/annotation/route.ts
536
- const _routeStore = /* @__PURE__ */ new WeakMap();
537
- /**
538
- * Custom annotation that will store all routes inside of a map,
539
- * which can then be used to initialize all the routes to the router.
540
- */
541
- function _Route({ method, path = "" }) {
542
- return (target, propertyKey) => {
543
- const ctor = target.constructor;
544
- const list = _routeStore.get(ctor) ?? [];
545
- list.push({
546
- method,
547
- path: path ?? "",
548
- fnName: String(propertyKey)
549
- });
550
- _routeStore.set(ctor, list);
551
- };
552
- }
553
- /**
554
- * Register GET route on the given path (default "") for the given controller.
555
- */
556
- const GET = (path = "") => _Route({
557
- method: "GET",
558
- path
559
- });
560
- /**
561
- * Register POST route on the given path (default "") for the given controller.
562
- */
563
- const POST = (path = "") => _Route({
564
- method: "POST",
565
- path
566
- });
567
- /**
568
- * Register PUT route on the given path (default "") for the given controller.
569
- */
570
- const PUT = (path = "") => _Route({
571
- method: "PUT",
572
- path
573
- });
574
- /**
575
- * Register DELETE route on the given path (default "") for the given controller.
576
- */
577
- const DELETE = (path = "") => _Route({
578
- method: "DELETE",
579
- path
580
- });
581
- /**
582
- * Register OPTIONS route on the given path (default "") for the given controller.
583
- */
584
- const OPTIONS = (path = "") => _Route({
585
- method: "OPTIONS",
586
- path
587
- });
588
- /**
589
- * Register PATCH route on the given path (default "") for the given controller.
590
- */
591
- const PATCH = (path = "") => _Route({
592
- method: "PATCH",
593
- path
594
- });
595
- /**
596
- * Register HEAD route on the given path (default "") for the given controller.
597
- */
598
- const HEAD = (path = "") => _Route({
599
- method: "HEAD",
600
- path
601
- });
602
- /**
603
- * Register a middleware route on the given path (default "") for the given controller.
604
- */
605
- const Middleware = (path = "") => _Route({
606
- method: "USE",
607
- path
608
- });
609
- /**
610
- * Given a class constructor, fetch all the routes attached.
611
- */
612
- function _getRoutes(ctor) {
613
- return _routeStore.get(ctor) ?? [];
614
- }
615
- //#endregion
616
780
  //#region src/annotation/controller.ts
617
781
  const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
618
782
  /**
@@ -624,6 +788,7 @@ const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
624
788
  function Controller({ prefix = "", deps = [] } = {}) {
625
789
  return (target) => {
626
790
  const targetClass = target;
791
+ _registerControllerClass(target, prefix);
627
792
  const router = Router();
628
793
  const routes = _getRoutes(target);
629
794
  const usedRoutes = /* @__PURE__ */ new Set();
@@ -715,7 +880,7 @@ function __decorate(decorators, target, key, desc) {
715
880
  return c > 3 && r && Object.defineProperty(target, key, r), r;
716
881
  }
717
882
  //#endregion
718
- //#region src/middleware/default/base.ts
883
+ //#region src/middleware/default/error/base.ts
719
884
  let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
720
885
  handle(err, _request, _response, _next) {
721
886
  console.error("[Error]", err);
@@ -725,7 +890,17 @@ let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
725
890
  __decorate([Middleware()], DefaultBaseErrorMiddleware.prototype, "handle", null);
726
891
  DefaultBaseErrorMiddleware = __decorate([MiddlewareClass()], DefaultBaseErrorMiddleware);
727
892
  //#endregion
728
- //#region src/middleware/default/responsestatus.ts
893
+ //#region src/middleware/default/error/parse.ts
894
+ let DefaultParserErrorMiddleware = class DefaultParserErrorMiddleware {
895
+ handle(err, _request, _response, next) {
896
+ if (err instanceof ParserError) return ResponseEntity.status(err.status).body({ message: err.message });
897
+ next(err);
898
+ }
899
+ };
900
+ __decorate([Middleware()], DefaultParserErrorMiddleware.prototype, "handle", null);
901
+ DefaultParserErrorMiddleware = __decorate([MiddlewareClass()], DefaultParserErrorMiddleware);
902
+ //#endregion
903
+ //#region src/middleware/default/error/responsestatus.ts
729
904
  let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddleware {
730
905
  handle(err, _request, _response, next) {
731
906
  if (err instanceof ResponseStatusError) return ResponseEntity.status(err.status).body({ message: err.message });
@@ -735,4 +910,13 @@ let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddl
735
910
  __decorate([Middleware()], DefaultResponseStatusErrorMiddleware.prototype, "handle", null);
736
911
  DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultResponseStatusErrorMiddleware);
737
912
  //#endregion
738
- 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 };
913
+ //#region src/middleware/default/openapi/index.ts
914
+ let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
915
+ handle(_request, _response, _next) {
916
+ return ResponseEntity.ok().body(generateOpenApiSpec());
917
+ }
918
+ };
919
+ __decorate([GET("/openapi.json")], DefaultOpenApiMiddleware.prototype, "handle", null);
920
+ DefaultOpenApiMiddleware = __decorate([MiddlewareClass()], DefaultOpenApiMiddleware);
921
+ //#endregion
922
+ export { Controller, DELETE, DefaultBaseErrorMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, 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, _registerControllerClass, _resolve, generateOpenApiSpec, methodResolve, openApiGenerator, setOpenApiConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tahminator/sapling",
3
- "version": "2.0.3",
3
+ "version": "2.0.5-beta.2f539758",
4
4
  "author": "Tahmid Ahmed",
5
5
  "description": "A library to help you write cleaner Express.js code",
6
6
  "repository": {
@@ -45,6 +45,7 @@
45
45
  "@types/express": "^5",
46
46
  "@types/supertest": "^7.2.0",
47
47
  "@vitest/coverage-istanbul": "^4.1.2",
48
+ "openapi-types": "12.1.3",
48
49
  "eslint": "^10.1.0",
49
50
  "eslint-plugin-perfectionist": "^5.7.0",
50
51
  "globals": "^17.4.0",
@@ -60,6 +61,7 @@
60
61
  "zod": "^4.4.3"
61
62
  },
62
63
  "inlinedDependencies": {
63
- "@standard-schema/spec": "1.1.0"
64
+ "@standard-schema/spec": "1.1.0",
65
+ "openapi-types": "12.1.3"
64
66
  }
65
67
  }