@tahminator/sapling 2.0.4 → 2.0.5-beta.23c37926

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
@@ -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.
@@ -238,9 +239,13 @@ var ParserError = class ParserError extends ResponseStatusError {
238
239
  };
239
240
  //#endregion
240
241
  //#region src/helper/sapling.ts
241
- const settings = {
242
+ const _settings = {
242
243
  serialize: JSON.stringify,
243
- deserialize: JSON.parse
244
+ deserialize: JSON.parse,
245
+ doc: {
246
+ openApiPath: "/openapi.json",
247
+ swaggerPath: "/swagger.html"
248
+ }
244
249
  };
245
250
  /**
246
251
  * Collection of utility functions which are essential for Sapling to function.
@@ -319,13 +324,13 @@ var Sapling = class Sapling {
319
324
  * @defaultValue `JSON.stringify`
320
325
  */
321
326
  static serialize(value) {
322
- return settings.serialize(value);
327
+ return _settings.serialize(value);
323
328
  }
324
329
  /**
325
330
  * Replace the function used for `serialize`.
326
331
  */
327
332
  static setSerializeFn(fn) {
328
- settings.serialize = fn;
333
+ _settings.serialize = fn;
329
334
  }
330
335
  /**
331
336
  * De-serialize a JSON string back to a JavaScript object.
@@ -337,148 +342,22 @@ var Sapling = class Sapling {
337
342
  * @defaultValue `JSON.parse`
338
343
  */
339
344
  static deserialize(value) {
340
- return settings.deserialize(value);
345
+ return _settings.deserialize(value);
341
346
  }
342
347
  /**
343
348
  * Replace the function used for `deserialize`
344
349
  */
345
350
  static setDeserializeFn(fn) {
346
- settings.deserialize = fn;
351
+ _settings.deserialize = fn;
347
352
  }
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);
353
+ static setOpenApiPath(path) {
354
+ _settings.doc.openApiPath = path;
372
355
  }
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);
356
+ static setSwaggerPath(path) {
357
+ _settings.doc.swaggerPath = path;
419
358
  }
420
359
  };
421
360
  //#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
361
  //#region src/annotation/request.ts
483
362
  const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
484
363
  /**
@@ -687,6 +566,229 @@ function _getRoutes(ctor) {
687
566
  return _routeStore.get(ctor) ?? [];
688
567
  }
689
568
  //#endregion
569
+ //#region src/helper/openapi.ts
570
+ var OpenAPIGenerator = class {
571
+ constructor() {
572
+ this.controllers = /* @__PURE__ */ new Set();
573
+ this.config = {
574
+ title: "API",
575
+ version: "1.0.0"
576
+ };
577
+ }
578
+ setConfig(config) {
579
+ this.config = config;
580
+ }
581
+ registerController(controllerClass, prefix) {
582
+ this.controllers.add({
583
+ class: controllerClass,
584
+ prefix
585
+ });
586
+ }
587
+ generateSpec() {
588
+ const config = this.config;
589
+ const paths = {};
590
+ for (const { class: controllerClass, prefix } of this.controllers) {
591
+ const routes = _getRoutes(controllerClass);
592
+ for (const route of routes) {
593
+ if (route.method === "USE") continue;
594
+ const schemas = _getRequestSchemas(controllerClass, route.fnName);
595
+ const fullPath = route.path instanceof RegExp ? route.path.source : prefix + route.path;
596
+ const openApiPath = typeof fullPath === "string" ? fullPath.replace(/:(\w+)/g, "{$1}") : fullPath;
597
+ if (!paths[openApiPath]) paths[openApiPath] = {};
598
+ const operation = { responses: { "200": { description: "Successful response" } } };
599
+ const parameters = [];
600
+ if (schemas?.param) {
601
+ const paramSchema = this.toJsonSchema(schemas.param);
602
+ if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) parameters.push({
603
+ name,
604
+ in: "path",
605
+ required: true,
606
+ schema
607
+ });
608
+ }
609
+ if (schemas?.query) {
610
+ const querySchema = this.toJsonSchema(schemas.query);
611
+ if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
612
+ const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
613
+ parameters.push({
614
+ name,
615
+ in: "query",
616
+ required: isRequired,
617
+ schema
618
+ });
619
+ }
620
+ }
621
+ if (parameters.length > 0) operation.parameters = parameters;
622
+ if (schemas?.body) operation.requestBody = {
623
+ required: true,
624
+ content: { "application/json": { schema: this.toJsonSchema(schemas.body) } }
625
+ };
626
+ const method = route.method.toLowerCase();
627
+ paths[openApiPath][method] = operation;
628
+ }
629
+ }
630
+ return {
631
+ openapi: "3.0.0",
632
+ info: {
633
+ title: config.title,
634
+ version: config.version,
635
+ description: config.description
636
+ },
637
+ paths
638
+ };
639
+ }
640
+ toJsonSchema(schema) {
641
+ try {
642
+ return schema["~standard"].jsonSchema.output({ target: "openapi-3.0" });
643
+ } catch (e) {
644
+ 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`);
645
+ throw e;
646
+ }
647
+ }
648
+ };
649
+ const openApiGenerator = new OpenAPIGenerator();
650
+ function _registerControllerClass(controllerClass, prefix) {
651
+ openApiGenerator.registerController(controllerClass, prefix);
652
+ }
653
+ function _setOpenApiConfig(config) {
654
+ openApiGenerator.setConfig(config);
655
+ }
656
+ function _generateOpenApiSpec() {
657
+ return openApiGenerator.generateSpec();
658
+ }
659
+ //#endregion
660
+ //#region src/types.ts
661
+ const methodResolve = {
662
+ GET: "get",
663
+ PUT: "put",
664
+ POST: "post",
665
+ DELETE: "delete",
666
+ OPTIONS: "options",
667
+ PATCH: "patch",
668
+ HEAD: "head",
669
+ USE: "use"
670
+ };
671
+ //#endregion
672
+ //#region lib/weakmap.ts
673
+ /**
674
+ * WeakMap that is iterable.
675
+ */
676
+ var IterableWeakMap = class IterableWeakMap {
677
+ #weakMap = /* @__PURE__ */ new WeakMap();
678
+ #refSet = /* @__PURE__ */ new Set();
679
+ #finalizationGroup = new FinalizationRegistry(IterableWeakMap.#cleanup);
680
+ static #cleanup(heldValue) {
681
+ heldValue.set.delete(heldValue.ref);
682
+ }
683
+ constructor(iterable) {
684
+ if (iterable) for (const [key, value] of iterable) this.set(key, value);
685
+ }
686
+ set(key, value) {
687
+ const ref = new WeakRef(key);
688
+ this.#weakMap.set(key, {
689
+ value,
690
+ ref
691
+ });
692
+ this.#refSet.add(ref);
693
+ this.#finalizationGroup.register(key, {
694
+ set: this.#refSet,
695
+ ref
696
+ }, ref);
697
+ return this;
698
+ }
699
+ get(key) {
700
+ return this.#weakMap.get(key)?.value;
701
+ }
702
+ delete(key) {
703
+ const entry = this.#weakMap.get(key);
704
+ if (!entry) return false;
705
+ this.#weakMap.delete(key);
706
+ this.#refSet.delete(entry.ref);
707
+ this.#finalizationGroup.unregister(entry.ref);
708
+ return true;
709
+ }
710
+ *[Symbol.iterator]() {
711
+ for (const ref of this.#refSet) {
712
+ const key = ref.deref();
713
+ if (!key) continue;
714
+ const entry = this.#weakMap.get(key);
715
+ if (entry) yield [key, entry.value];
716
+ }
717
+ }
718
+ entries() {
719
+ return this[Symbol.iterator]();
720
+ }
721
+ *keys() {
722
+ for (const [key] of this) yield key;
723
+ }
724
+ *values() {
725
+ for (const [, value] of this) yield value;
726
+ }
727
+ forEach(callback, thisArg) {
728
+ for (const [key, value] of this) callback.call(thisArg, value, key, this);
729
+ }
730
+ };
731
+ //#endregion
732
+ //#region src/annotation/injectable.ts
733
+ const _InjectableRegistry = /* @__PURE__ */ new WeakMap();
734
+ const _InjectableDeps = new IterableWeakMap();
735
+ /**
736
+ * Mark the class as an injectable to be handled by Sapling. The class can now be
737
+ * be injected into other classes, as well as allow the class to inject other `@Injectable` classes.
738
+ *
739
+ * @argument deps - An optional array to define any dependencies that this class may require.
740
+ */
741
+ function Injectable(deps = []) {
742
+ return function(target) {
743
+ _InjectableRegistry.set(target, null);
744
+ _InjectableDeps.set(target, deps);
745
+ };
746
+ }
747
+ /**
748
+ * Resolves and instantiates a class along with all of it's transitive dependencies.
749
+ *
750
+ * Uses topological sort (Kahn's algorithm) to ensure that the dependency graph is created
751
+ * in a correct order.
752
+ *
753
+ * When `resolve` is first called (usually during controller registration),
754
+ * it will compute the dependency graph of all `@Injectable` classes and instantiates
755
+ * them in the correct order.
756
+ *
757
+ * Subsequent calls to dependencies that have already been resolved are cached, so they will
758
+ * re-use the created singletons instead of re-instantiation.
759
+ */
760
+ function _resolve(ctor) {
761
+ const inDegree = /* @__PURE__ */ new Map();
762
+ const graph = /* @__PURE__ */ new Map();
763
+ _InjectableDeps.forEach((deps, node) => {
764
+ inDegree.set(node, inDegree.get(node) || 0);
765
+ deps.forEach((dep) => {
766
+ 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.`);
767
+ inDegree.set(dep, inDegree.get(dep) || 0);
768
+ inDegree.set(node, inDegree.get(node) + 1);
769
+ if (!graph.has(dep)) graph.set(dep, []);
770
+ graph.get(dep).push(node);
771
+ });
772
+ });
773
+ const queue = [];
774
+ inDegree.forEach((deg, node) => {
775
+ if (deg === 0) queue.push(node);
776
+ });
777
+ while (queue.length) {
778
+ const current = queue.shift();
779
+ if (!_InjectableRegistry.get(current)) {
780
+ const instance = new current(...(_InjectableDeps.get(current) || []).map((dep) => _InjectableRegistry.get(dep)));
781
+ _InjectableRegistry.set(current, instance);
782
+ }
783
+ (graph.get(current) || []).forEach((neighbor) => {
784
+ inDegree.set(neighbor, (inDegree.get(neighbor) ?? 0) - 1);
785
+ if (inDegree.get(neighbor) === 0) queue.push(neighbor);
786
+ });
787
+ }
788
+ if (!_InjectableRegistry.get(ctor)) throw new Error("Circular dependency detected or injectable not registered");
789
+ return _InjectableRegistry.get(ctor);
790
+ }
791
+ //#endregion
690
792
  //#region src/annotation/controller.ts
691
793
  const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
692
794
  /**
@@ -698,6 +800,7 @@ const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
698
800
  function Controller({ prefix = "", deps = [] } = {}) {
699
801
  return (target) => {
700
802
  const targetClass = target;
803
+ _registerControllerClass(target, prefix);
701
804
  const router = Router();
702
805
  const routes = _getRoutes(target);
703
806
  const usedRoutes = /* @__PURE__ */ new Set();
@@ -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/responsestatus.ts
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
- 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 };
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 this.handlers;
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(void 0, { 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, DELETE, DefaultBaseErrorMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, GET, HEAD, Html404ErrorPage, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _generateOpenApiSpec, _getRequestSchemas, _getRoutes, _parseOrThrow, _registerControllerClass, _resolve, _setOpenApiConfig, _settings, methodResolve, openApiGenerator };
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.23c37926",
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
  }