@tahminator/sapling 2.0.4 → 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,140 +346,11 @@ var Sapling = class Sapling {
345
346
  static setDeserializeFn(fn) {
346
347
  settings.deserialize = fn;
347
348
  }
348
- };
349
- //#endregion
350
- //#region src/types.ts
351
- const methodResolve = {
352
- GET: "get",
353
- PUT: "put",
354
- POST: "post",
355
- DELETE: "delete",
356
- OPTIONS: "options",
357
- PATCH: "patch",
358
- HEAD: "head",
359
- USE: "use"
360
- };
361
- //#endregion
362
- //#region lib/weakmap.ts
363
- /**
364
- * WeakMap that is iterable.
365
- */
366
- var IterableWeakMap = class IterableWeakMap {
367
- #weakMap = /* @__PURE__ */ new WeakMap();
368
- #refSet = /* @__PURE__ */ new Set();
369
- #finalizationGroup = new FinalizationRegistry(IterableWeakMap.#cleanup);
370
- static #cleanup(heldValue) {
371
- heldValue.set.delete(heldValue.ref);
372
- }
373
- constructor(iterable) {
374
- if (iterable) for (const [key, value] of iterable) this.set(key, value);
375
- }
376
- set(key, value) {
377
- const ref = new WeakRef(key);
378
- this.#weakMap.set(key, {
379
- value,
380
- ref
381
- });
382
- this.#refSet.add(ref);
383
- this.#finalizationGroup.register(key, {
384
- set: this.#refSet,
385
- ref
386
- }, ref);
387
- return this;
388
- }
389
- get(key) {
390
- return this.#weakMap.get(key)?.value;
391
- }
392
- delete(key) {
393
- const entry = this.#weakMap.get(key);
394
- if (!entry) return false;
395
- this.#weakMap.delete(key);
396
- this.#refSet.delete(entry.ref);
397
- this.#finalizationGroup.unregister(entry.ref);
398
- return true;
399
- }
400
- *[Symbol.iterator]() {
401
- for (const ref of this.#refSet) {
402
- const key = ref.deref();
403
- if (!key) continue;
404
- const entry = this.#weakMap.get(key);
405
- if (entry) yield [key, entry.value];
406
- }
407
- }
408
- entries() {
409
- return this[Symbol.iterator]();
410
- }
411
- *keys() {
412
- for (const [key] of this) yield key;
413
- }
414
- *values() {
415
- for (const [, value] of this) yield value;
416
- }
417
- forEach(callback, thisArg) {
418
- for (const [key, value] of this) callback.call(thisArg, value, key, this);
349
+ static setOpenApiPath(path) {
350
+ settings.openapi.path = path;
419
351
  }
420
352
  };
421
353
  //#endregion
422
- //#region src/annotation/injectable.ts
423
- const _InjectableRegistry = /* @__PURE__ */ new WeakMap();
424
- const _InjectableDeps = new IterableWeakMap();
425
- /**
426
- * Mark the class as an injectable to be handled by Sapling. The class can now be
427
- * be injected into other classes, as well as allow the class to inject other `@Injectable` classes.
428
- *
429
- * @argument deps - An optional array to define any dependencies that this class may require.
430
- */
431
- function Injectable(deps = []) {
432
- return function(target) {
433
- _InjectableRegistry.set(target, null);
434
- _InjectableDeps.set(target, deps);
435
- };
436
- }
437
- /**
438
- * Resolves and instantiates a class along with all of it's transitive dependencies.
439
- *
440
- * Uses topological sort (Kahn's algorithm) to ensure that the dependency graph is created
441
- * in a correct order.
442
- *
443
- * When `resolve` is first called (usually during controller registration),
444
- * it will compute the dependency graph of all `@Injectable` classes and instantiates
445
- * them in the correct order.
446
- *
447
- * Subsequent calls to dependencies that have already been resolved are cached, so they will
448
- * re-use the created singletons instead of re-instantiation.
449
- */
450
- function _resolve(ctor) {
451
- const inDegree = /* @__PURE__ */ new Map();
452
- const graph = /* @__PURE__ */ new Map();
453
- _InjectableDeps.forEach((deps, node) => {
454
- inDegree.set(node, inDegree.get(node) || 0);
455
- deps.forEach((dep) => {
456
- if (dep === void 0) throw new Error(`There is an @Injectable (${node.name}) which has a dependency that cannot be found. This is likely caused by a circular dependency.`);
457
- inDegree.set(dep, inDegree.get(dep) || 0);
458
- inDegree.set(node, inDegree.get(node) + 1);
459
- if (!graph.has(dep)) graph.set(dep, []);
460
- graph.get(dep).push(node);
461
- });
462
- });
463
- const queue = [];
464
- inDegree.forEach((deg, node) => {
465
- if (deg === 0) queue.push(node);
466
- });
467
- while (queue.length) {
468
- const current = queue.shift();
469
- if (!_InjectableRegistry.get(current)) {
470
- const instance = new current(...(_InjectableDeps.get(current) || []).map((dep) => _InjectableRegistry.get(dep)));
471
- _InjectableRegistry.set(current, instance);
472
- }
473
- (graph.get(current) || []).forEach((neighbor) => {
474
- inDegree.set(neighbor, (inDegree.get(neighbor) ?? 0) - 1);
475
- if (inDegree.get(neighbor) === 0) queue.push(neighbor);
476
- });
477
- }
478
- if (!_InjectableRegistry.get(ctor)) throw new Error("Circular dependency detected or injectable not registered");
479
- return _InjectableRegistry.get(ctor);
480
- }
481
- //#endregion
482
354
  //#region src/annotation/request.ts
483
355
  const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
484
356
  /**
@@ -687,6 +559,224 @@ function _getRoutes(ctor) {
687
559
  return _routeStore.get(ctor) ?? [];
688
560
  }
689
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
+ }
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
+ }
647
+ //#endregion
648
+ //#region src/types.ts
649
+ const methodResolve = {
650
+ GET: "get",
651
+ PUT: "put",
652
+ POST: "post",
653
+ DELETE: "delete",
654
+ OPTIONS: "options",
655
+ PATCH: "patch",
656
+ HEAD: "head",
657
+ USE: "use"
658
+ };
659
+ //#endregion
660
+ //#region lib/weakmap.ts
661
+ /**
662
+ * WeakMap that is iterable.
663
+ */
664
+ var IterableWeakMap = class IterableWeakMap {
665
+ #weakMap = /* @__PURE__ */ new WeakMap();
666
+ #refSet = /* @__PURE__ */ new Set();
667
+ #finalizationGroup = new FinalizationRegistry(IterableWeakMap.#cleanup);
668
+ static #cleanup(heldValue) {
669
+ heldValue.set.delete(heldValue.ref);
670
+ }
671
+ constructor(iterable) {
672
+ if (iterable) for (const [key, value] of iterable) this.set(key, value);
673
+ }
674
+ set(key, value) {
675
+ const ref = new WeakRef(key);
676
+ this.#weakMap.set(key, {
677
+ value,
678
+ ref
679
+ });
680
+ this.#refSet.add(ref);
681
+ this.#finalizationGroup.register(key, {
682
+ set: this.#refSet,
683
+ ref
684
+ }, ref);
685
+ return this;
686
+ }
687
+ get(key) {
688
+ return this.#weakMap.get(key)?.value;
689
+ }
690
+ delete(key) {
691
+ const entry = this.#weakMap.get(key);
692
+ if (!entry) return false;
693
+ this.#weakMap.delete(key);
694
+ this.#refSet.delete(entry.ref);
695
+ this.#finalizationGroup.unregister(entry.ref);
696
+ return true;
697
+ }
698
+ *[Symbol.iterator]() {
699
+ for (const ref of this.#refSet) {
700
+ const key = ref.deref();
701
+ if (!key) continue;
702
+ const entry = this.#weakMap.get(key);
703
+ if (entry) yield [key, entry.value];
704
+ }
705
+ }
706
+ entries() {
707
+ return this[Symbol.iterator]();
708
+ }
709
+ *keys() {
710
+ for (const [key] of this) yield key;
711
+ }
712
+ *values() {
713
+ for (const [, value] of this) yield value;
714
+ }
715
+ forEach(callback, thisArg) {
716
+ for (const [key, value] of this) callback.call(thisArg, value, key, this);
717
+ }
718
+ };
719
+ //#endregion
720
+ //#region src/annotation/injectable.ts
721
+ const _InjectableRegistry = /* @__PURE__ */ new WeakMap();
722
+ const _InjectableDeps = new IterableWeakMap();
723
+ /**
724
+ * Mark the class as an injectable to be handled by Sapling. The class can now be
725
+ * be injected into other classes, as well as allow the class to inject other `@Injectable` classes.
726
+ *
727
+ * @argument deps - An optional array to define any dependencies that this class may require.
728
+ */
729
+ function Injectable(deps = []) {
730
+ return function(target) {
731
+ _InjectableRegistry.set(target, null);
732
+ _InjectableDeps.set(target, deps);
733
+ };
734
+ }
735
+ /**
736
+ * Resolves and instantiates a class along with all of it's transitive dependencies.
737
+ *
738
+ * Uses topological sort (Kahn's algorithm) to ensure that the dependency graph is created
739
+ * in a correct order.
740
+ *
741
+ * When `resolve` is first called (usually during controller registration),
742
+ * it will compute the dependency graph of all `@Injectable` classes and instantiates
743
+ * them in the correct order.
744
+ *
745
+ * Subsequent calls to dependencies that have already been resolved are cached, so they will
746
+ * re-use the created singletons instead of re-instantiation.
747
+ */
748
+ function _resolve(ctor) {
749
+ const inDegree = /* @__PURE__ */ new Map();
750
+ const graph = /* @__PURE__ */ new Map();
751
+ _InjectableDeps.forEach((deps, node) => {
752
+ inDegree.set(node, inDegree.get(node) || 0);
753
+ deps.forEach((dep) => {
754
+ if (dep === void 0) throw new Error(`There is an @Injectable (${node.name}) which has a dependency that cannot be found. This is likely caused by a circular dependency.`);
755
+ inDegree.set(dep, inDegree.get(dep) || 0);
756
+ inDegree.set(node, inDegree.get(node) + 1);
757
+ if (!graph.has(dep)) graph.set(dep, []);
758
+ graph.get(dep).push(node);
759
+ });
760
+ });
761
+ const queue = [];
762
+ inDegree.forEach((deg, node) => {
763
+ if (deg === 0) queue.push(node);
764
+ });
765
+ while (queue.length) {
766
+ const current = queue.shift();
767
+ if (!_InjectableRegistry.get(current)) {
768
+ const instance = new current(...(_InjectableDeps.get(current) || []).map((dep) => _InjectableRegistry.get(dep)));
769
+ _InjectableRegistry.set(current, instance);
770
+ }
771
+ (graph.get(current) || []).forEach((neighbor) => {
772
+ inDegree.set(neighbor, (inDegree.get(neighbor) ?? 0) - 1);
773
+ if (inDegree.get(neighbor) === 0) queue.push(neighbor);
774
+ });
775
+ }
776
+ if (!_InjectableRegistry.get(ctor)) throw new Error("Circular dependency detected or injectable not registered");
777
+ return _InjectableRegistry.get(ctor);
778
+ }
779
+ //#endregion
690
780
  //#region src/annotation/controller.ts
691
781
  const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
692
782
  /**
@@ -698,6 +788,7 @@ const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
698
788
  function Controller({ prefix = "", deps = [] } = {}) {
699
789
  return (target) => {
700
790
  const targetClass = target;
791
+ _registerControllerClass(target, prefix);
701
792
  const router = Router();
702
793
  const routes = _getRoutes(target);
703
794
  const usedRoutes = /* @__PURE__ */ new Set();
@@ -789,7 +880,7 @@ function __decorate(decorators, target, key, desc) {
789
880
  return c > 3 && r && Object.defineProperty(target, key, r), r;
790
881
  }
791
882
  //#endregion
792
- //#region src/middleware/default/base.ts
883
+ //#region src/middleware/default/error/base.ts
793
884
  let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
794
885
  handle(err, _request, _response, _next) {
795
886
  console.error("[Error]", err);
@@ -799,7 +890,17 @@ let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
799
890
  __decorate([Middleware()], DefaultBaseErrorMiddleware.prototype, "handle", null);
800
891
  DefaultBaseErrorMiddleware = __decorate([MiddlewareClass()], DefaultBaseErrorMiddleware);
801
892
  //#endregion
802
- //#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
803
904
  let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddleware {
804
905
  handle(err, _request, _response, next) {
805
906
  if (err instanceof ResponseStatusError) return ResponseEntity.status(err.status).body({ message: err.message });
@@ -809,4 +910,13 @@ let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddl
809
910
  __decorate([Middleware()], DefaultResponseStatusErrorMiddleware.prototype, "handle", null);
810
911
  DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultResponseStatusErrorMiddleware);
811
912
  //#endregion
812
- 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.4",
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
  }