@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/README.md +94 -4
- package/dist/index.cjs +305 -138
- package/dist/index.d.cts +429 -13
- package/dist/index.d.mts +429 -13
- package/dist/index.mjs +287 -139
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -200,12 +200,71 @@ class UserController {
|
|
|
200
200
|
}
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
Sapling ships with default error middlewares, and you can also write your own.
|
|
204
|
+
Register error middlewares after your regular middlewares and controllers:
|
|
204
205
|
|
|
205
206
|
```typescript
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
import {
|
|
208
|
+
DefaultBaseErrorMiddleware,
|
|
209
|
+
DefaultResponseStatusErrorMiddleware,
|
|
210
|
+
} from "@tahminator/sapling";
|
|
211
|
+
|
|
212
|
+
// regular middlewares & controllers first
|
|
213
|
+
const middlewares: Class<any>[] = [CookieParserMiddleware];
|
|
214
|
+
middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
215
|
+
|
|
216
|
+
const controllers: Class<any>[] = [UserController];
|
|
217
|
+
controllers.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
218
|
+
|
|
219
|
+
// error middlewares last
|
|
220
|
+
const errorMiddlewares: Class<any>[] = [
|
|
221
|
+
DefaultResponseStatusErrorMiddleware,
|
|
222
|
+
DefaultBaseErrorMiddleware,
|
|
223
|
+
];
|
|
224
|
+
errorMiddlewares.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
You can also write your own error middlewares. A specific handler should call
|
|
228
|
+
`next(err)` when it does not handle the error, and a base handler should be last
|
|
229
|
+
and return a response:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
@MiddlewareClass()
|
|
233
|
+
class ResponseStatusErrorMiddleware {
|
|
234
|
+
@Middleware()
|
|
235
|
+
handle(
|
|
236
|
+
err: unknown,
|
|
237
|
+
_request: Request,
|
|
238
|
+
_response: Response,
|
|
239
|
+
next: NextFunction,
|
|
240
|
+
) {
|
|
241
|
+
if (err instanceof ResponseStatusError) {
|
|
242
|
+
return ResponseEntity.status(err.status).body({ message: err.message });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// MUST call next(err) to continue the chain
|
|
246
|
+
next(err);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@MiddlewareClass()
|
|
251
|
+
class BaseErrorMiddleware {
|
|
252
|
+
@Middleware()
|
|
253
|
+
handle(
|
|
254
|
+
err: unknown,
|
|
255
|
+
_request: Request,
|
|
256
|
+
_response: Response,
|
|
257
|
+
_next: NextFunction,
|
|
258
|
+
) {
|
|
259
|
+
console.error("[Error]", err);
|
|
260
|
+
|
|
261
|
+
return ResponseEntity.status(500).body({
|
|
262
|
+
message: "Internal Server Error",
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// no next(err) since last middleware in chain, we are done propagating
|
|
266
|
+
}
|
|
267
|
+
}
|
|
209
268
|
```
|
|
210
269
|
|
|
211
270
|
### Middleware
|
|
@@ -234,10 +293,41 @@ class CookieParserMiddleware {
|
|
|
234
293
|
// Register it like any controller
|
|
235
294
|
app.use(Sapling.resolve(CookieParserMiddleware));
|
|
236
295
|
|
|
296
|
+
// Register middlewares before controllers
|
|
297
|
+
app.use(Sapling.resolve(UserController));
|
|
298
|
+
|
|
237
299
|
// You can also still choose to load plugins the Express.js way
|
|
238
300
|
app.use(cookieParser());
|
|
239
301
|
```
|
|
240
302
|
|
|
303
|
+
You can also write custom middlewares as well. It is functionally the same way as Express: call `next()` explicitly to
|
|
304
|
+
continue down the chain:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { MiddlewareClass, Middleware } from "@tahminator/sapling";
|
|
308
|
+
import { NextFunction, Request, Response } from "express";
|
|
309
|
+
|
|
310
|
+
@MiddlewareClass()
|
|
311
|
+
class RequestTimerMiddleware {
|
|
312
|
+
@Middleware()
|
|
313
|
+
handle(request: Request, _response: Response, next: NextFunction) {
|
|
314
|
+
const start = Date.now();
|
|
315
|
+
|
|
316
|
+
request.on("finish", () => {
|
|
317
|
+
const elapsedMs = Date.now() - start;
|
|
318
|
+
console.log(`[Request] ${request.method} ${request.path} ${elapsedMs}ms`);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// MUST call next() to continue the chain
|
|
322
|
+
next();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Register middlewares before controllers
|
|
327
|
+
app.use(Sapling.resolve(RequestTimerMiddleware));
|
|
328
|
+
app.use(Sapling.resolve(UserController));
|
|
329
|
+
```
|
|
330
|
+
|
|
241
331
|
### Request Validation
|
|
242
332
|
|
|
243
333
|
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.).
|
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
//#endregion
|
|
24
24
|
let express = require("express");
|
|
25
25
|
express = __toESM(express);
|
|
26
|
+
let swagger_ui_express = require("swagger-ui-express");
|
|
27
|
+
swagger_ui_express = __toESM(swagger_ui_express);
|
|
26
28
|
//#region src/html/404.ts
|
|
27
29
|
/**
|
|
28
30
|
* Default Express.js 404 error page, as a string.
|
|
@@ -262,9 +264,13 @@ var ParserError = class ParserError extends ResponseStatusError {
|
|
|
262
264
|
};
|
|
263
265
|
//#endregion
|
|
264
266
|
//#region src/helper/sapling.ts
|
|
265
|
-
const
|
|
267
|
+
const _settings = {
|
|
266
268
|
serialize: JSON.stringify,
|
|
267
|
-
deserialize: JSON.parse
|
|
269
|
+
deserialize: JSON.parse,
|
|
270
|
+
doc: {
|
|
271
|
+
openApiPath: "/openapi.json",
|
|
272
|
+
swaggerPath: "/swagger.html"
|
|
273
|
+
}
|
|
268
274
|
};
|
|
269
275
|
/**
|
|
270
276
|
* Collection of utility functions which are essential for Sapling to function.
|
|
@@ -343,13 +349,13 @@ var Sapling = class Sapling {
|
|
|
343
349
|
* @defaultValue `JSON.stringify`
|
|
344
350
|
*/
|
|
345
351
|
static serialize(value) {
|
|
346
|
-
return
|
|
352
|
+
return _settings.serialize(value);
|
|
347
353
|
}
|
|
348
354
|
/**
|
|
349
355
|
* Replace the function used for `serialize`.
|
|
350
356
|
*/
|
|
351
357
|
static setSerializeFn(fn) {
|
|
352
|
-
|
|
358
|
+
_settings.serialize = fn;
|
|
353
359
|
}
|
|
354
360
|
/**
|
|
355
361
|
* De-serialize a JSON string back to a JavaScript object.
|
|
@@ -361,148 +367,22 @@ var Sapling = class Sapling {
|
|
|
361
367
|
* @defaultValue `JSON.parse`
|
|
362
368
|
*/
|
|
363
369
|
static deserialize(value) {
|
|
364
|
-
return
|
|
370
|
+
return _settings.deserialize(value);
|
|
365
371
|
}
|
|
366
372
|
/**
|
|
367
373
|
* Replace the function used for `deserialize`
|
|
368
374
|
*/
|
|
369
375
|
static setDeserializeFn(fn) {
|
|
370
|
-
|
|
376
|
+
_settings.deserialize = fn;
|
|
371
377
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
//#region src/types.ts
|
|
375
|
-
const methodResolve = {
|
|
376
|
-
GET: "get",
|
|
377
|
-
PUT: "put",
|
|
378
|
-
POST: "post",
|
|
379
|
-
DELETE: "delete",
|
|
380
|
-
OPTIONS: "options",
|
|
381
|
-
PATCH: "patch",
|
|
382
|
-
HEAD: "head",
|
|
383
|
-
USE: "use"
|
|
384
|
-
};
|
|
385
|
-
//#endregion
|
|
386
|
-
//#region lib/weakmap.ts
|
|
387
|
-
/**
|
|
388
|
-
* WeakMap that is iterable.
|
|
389
|
-
*/
|
|
390
|
-
var IterableWeakMap = class IterableWeakMap {
|
|
391
|
-
#weakMap = /* @__PURE__ */ new WeakMap();
|
|
392
|
-
#refSet = /* @__PURE__ */ new Set();
|
|
393
|
-
#finalizationGroup = new FinalizationRegistry(IterableWeakMap.#cleanup);
|
|
394
|
-
static #cleanup(heldValue) {
|
|
395
|
-
heldValue.set.delete(heldValue.ref);
|
|
378
|
+
static setOpenApiPath(path) {
|
|
379
|
+
_settings.doc.openApiPath = path;
|
|
396
380
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
set(key, value) {
|
|
401
|
-
const ref = new WeakRef(key);
|
|
402
|
-
this.#weakMap.set(key, {
|
|
403
|
-
value,
|
|
404
|
-
ref
|
|
405
|
-
});
|
|
406
|
-
this.#refSet.add(ref);
|
|
407
|
-
this.#finalizationGroup.register(key, {
|
|
408
|
-
set: this.#refSet,
|
|
409
|
-
ref
|
|
410
|
-
}, ref);
|
|
411
|
-
return this;
|
|
412
|
-
}
|
|
413
|
-
get(key) {
|
|
414
|
-
return this.#weakMap.get(key)?.value;
|
|
415
|
-
}
|
|
416
|
-
delete(key) {
|
|
417
|
-
const entry = this.#weakMap.get(key);
|
|
418
|
-
if (!entry) return false;
|
|
419
|
-
this.#weakMap.delete(key);
|
|
420
|
-
this.#refSet.delete(entry.ref);
|
|
421
|
-
this.#finalizationGroup.unregister(entry.ref);
|
|
422
|
-
return true;
|
|
423
|
-
}
|
|
424
|
-
*[Symbol.iterator]() {
|
|
425
|
-
for (const ref of this.#refSet) {
|
|
426
|
-
const key = ref.deref();
|
|
427
|
-
if (!key) continue;
|
|
428
|
-
const entry = this.#weakMap.get(key);
|
|
429
|
-
if (entry) yield [key, entry.value];
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
entries() {
|
|
433
|
-
return this[Symbol.iterator]();
|
|
434
|
-
}
|
|
435
|
-
*keys() {
|
|
436
|
-
for (const [key] of this) yield key;
|
|
437
|
-
}
|
|
438
|
-
*values() {
|
|
439
|
-
for (const [, value] of this) yield value;
|
|
440
|
-
}
|
|
441
|
-
forEach(callback, thisArg) {
|
|
442
|
-
for (const [key, value] of this) callback.call(thisArg, value, key, this);
|
|
381
|
+
static setSwaggerPath(path) {
|
|
382
|
+
_settings.doc.swaggerPath = path;
|
|
443
383
|
}
|
|
444
384
|
};
|
|
445
385
|
//#endregion
|
|
446
|
-
//#region src/annotation/injectable.ts
|
|
447
|
-
const _InjectableRegistry = /* @__PURE__ */ new WeakMap();
|
|
448
|
-
const _InjectableDeps = new IterableWeakMap();
|
|
449
|
-
/**
|
|
450
|
-
* Mark the class as an injectable to be handled by Sapling. The class can now be
|
|
451
|
-
* be injected into other classes, as well as allow the class to inject other `@Injectable` classes.
|
|
452
|
-
*
|
|
453
|
-
* @argument deps - An optional array to define any dependencies that this class may require.
|
|
454
|
-
*/
|
|
455
|
-
function Injectable(deps = []) {
|
|
456
|
-
return function(target) {
|
|
457
|
-
_InjectableRegistry.set(target, null);
|
|
458
|
-
_InjectableDeps.set(target, deps);
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Resolves and instantiates a class along with all of it's transitive dependencies.
|
|
463
|
-
*
|
|
464
|
-
* Uses topological sort (Kahn's algorithm) to ensure that the dependency graph is created
|
|
465
|
-
* in a correct order.
|
|
466
|
-
*
|
|
467
|
-
* When `resolve` is first called (usually during controller registration),
|
|
468
|
-
* it will compute the dependency graph of all `@Injectable` classes and instantiates
|
|
469
|
-
* them in the correct order.
|
|
470
|
-
*
|
|
471
|
-
* Subsequent calls to dependencies that have already been resolved are cached, so they will
|
|
472
|
-
* re-use the created singletons instead of re-instantiation.
|
|
473
|
-
*/
|
|
474
|
-
function _resolve(ctor) {
|
|
475
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
476
|
-
const graph = /* @__PURE__ */ new Map();
|
|
477
|
-
_InjectableDeps.forEach((deps, node) => {
|
|
478
|
-
inDegree.set(node, inDegree.get(node) || 0);
|
|
479
|
-
deps.forEach((dep) => {
|
|
480
|
-
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.`);
|
|
481
|
-
inDegree.set(dep, inDegree.get(dep) || 0);
|
|
482
|
-
inDegree.set(node, inDegree.get(node) + 1);
|
|
483
|
-
if (!graph.has(dep)) graph.set(dep, []);
|
|
484
|
-
graph.get(dep).push(node);
|
|
485
|
-
});
|
|
486
|
-
});
|
|
487
|
-
const queue = [];
|
|
488
|
-
inDegree.forEach((deg, node) => {
|
|
489
|
-
if (deg === 0) queue.push(node);
|
|
490
|
-
});
|
|
491
|
-
while (queue.length) {
|
|
492
|
-
const current = queue.shift();
|
|
493
|
-
if (!_InjectableRegistry.get(current)) {
|
|
494
|
-
const instance = new current(...(_InjectableDeps.get(current) || []).map((dep) => _InjectableRegistry.get(dep)));
|
|
495
|
-
_InjectableRegistry.set(current, instance);
|
|
496
|
-
}
|
|
497
|
-
(graph.get(current) || []).forEach((neighbor) => {
|
|
498
|
-
inDegree.set(neighbor, (inDegree.get(neighbor) ?? 0) - 1);
|
|
499
|
-
if (inDegree.get(neighbor) === 0) queue.push(neighbor);
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
if (!_InjectableRegistry.get(ctor)) throw new Error("Circular dependency detected or injectable not registered");
|
|
503
|
-
return _InjectableRegistry.get(ctor);
|
|
504
|
-
}
|
|
505
|
-
//#endregion
|
|
506
386
|
//#region src/annotation/request.ts
|
|
507
387
|
const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
508
388
|
/**
|
|
@@ -711,6 +591,229 @@ function _getRoutes(ctor) {
|
|
|
711
591
|
return _routeStore.get(ctor) ?? [];
|
|
712
592
|
}
|
|
713
593
|
//#endregion
|
|
594
|
+
//#region src/helper/openapi.ts
|
|
595
|
+
var OpenAPIGenerator = class {
|
|
596
|
+
constructor() {
|
|
597
|
+
this.controllers = /* @__PURE__ */ new Set();
|
|
598
|
+
this.config = {
|
|
599
|
+
title: "API",
|
|
600
|
+
version: "1.0.0"
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
setConfig(config) {
|
|
604
|
+
this.config = config;
|
|
605
|
+
}
|
|
606
|
+
registerController(controllerClass, prefix) {
|
|
607
|
+
this.controllers.add({
|
|
608
|
+
class: controllerClass,
|
|
609
|
+
prefix
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
generateSpec() {
|
|
613
|
+
const config = this.config;
|
|
614
|
+
const paths = {};
|
|
615
|
+
for (const { class: controllerClass, prefix } of this.controllers) {
|
|
616
|
+
const routes = _getRoutes(controllerClass);
|
|
617
|
+
for (const route of routes) {
|
|
618
|
+
if (route.method === "USE") continue;
|
|
619
|
+
const schemas = _getRequestSchemas(controllerClass, route.fnName);
|
|
620
|
+
const fullPath = route.path instanceof RegExp ? route.path.source : prefix + route.path;
|
|
621
|
+
const openApiPath = typeof fullPath === "string" ? fullPath.replace(/:(\w+)/g, "{$1}") : fullPath;
|
|
622
|
+
if (!paths[openApiPath]) paths[openApiPath] = {};
|
|
623
|
+
const operation = { responses: { "200": { description: "Successful response" } } };
|
|
624
|
+
const parameters = [];
|
|
625
|
+
if (schemas?.param) {
|
|
626
|
+
const paramSchema = this.toJsonSchema(schemas.param);
|
|
627
|
+
if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) parameters.push({
|
|
628
|
+
name,
|
|
629
|
+
in: "path",
|
|
630
|
+
required: true,
|
|
631
|
+
schema
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
if (schemas?.query) {
|
|
635
|
+
const querySchema = this.toJsonSchema(schemas.query);
|
|
636
|
+
if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
|
|
637
|
+
const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
|
|
638
|
+
parameters.push({
|
|
639
|
+
name,
|
|
640
|
+
in: "query",
|
|
641
|
+
required: isRequired,
|
|
642
|
+
schema
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (parameters.length > 0) operation.parameters = parameters;
|
|
647
|
+
if (schemas?.body) operation.requestBody = {
|
|
648
|
+
required: true,
|
|
649
|
+
content: { "application/json": { schema: this.toJsonSchema(schemas.body) } }
|
|
650
|
+
};
|
|
651
|
+
const method = route.method.toLowerCase();
|
|
652
|
+
paths[openApiPath][method] = operation;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
openapi: "3.0.0",
|
|
657
|
+
info: {
|
|
658
|
+
title: config.title,
|
|
659
|
+
version: config.version,
|
|
660
|
+
description: config.description
|
|
661
|
+
},
|
|
662
|
+
paths
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
toJsonSchema(schema) {
|
|
666
|
+
try {
|
|
667
|
+
return schema["~standard"].jsonSchema.output({ target: "openapi-3.0" });
|
|
668
|
+
} catch (e) {
|
|
669
|
+
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`);
|
|
670
|
+
throw e;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
const openApiGenerator = new OpenAPIGenerator();
|
|
675
|
+
function _registerControllerClass(controllerClass, prefix) {
|
|
676
|
+
openApiGenerator.registerController(controllerClass, prefix);
|
|
677
|
+
}
|
|
678
|
+
function _setOpenApiConfig(config) {
|
|
679
|
+
openApiGenerator.setConfig(config);
|
|
680
|
+
}
|
|
681
|
+
function _generateOpenApiSpec() {
|
|
682
|
+
return openApiGenerator.generateSpec();
|
|
683
|
+
}
|
|
684
|
+
//#endregion
|
|
685
|
+
//#region src/types.ts
|
|
686
|
+
const methodResolve = {
|
|
687
|
+
GET: "get",
|
|
688
|
+
PUT: "put",
|
|
689
|
+
POST: "post",
|
|
690
|
+
DELETE: "delete",
|
|
691
|
+
OPTIONS: "options",
|
|
692
|
+
PATCH: "patch",
|
|
693
|
+
HEAD: "head",
|
|
694
|
+
USE: "use"
|
|
695
|
+
};
|
|
696
|
+
//#endregion
|
|
697
|
+
//#region lib/weakmap.ts
|
|
698
|
+
/**
|
|
699
|
+
* WeakMap that is iterable.
|
|
700
|
+
*/
|
|
701
|
+
var IterableWeakMap = class IterableWeakMap {
|
|
702
|
+
#weakMap = /* @__PURE__ */ new WeakMap();
|
|
703
|
+
#refSet = /* @__PURE__ */ new Set();
|
|
704
|
+
#finalizationGroup = new FinalizationRegistry(IterableWeakMap.#cleanup);
|
|
705
|
+
static #cleanup(heldValue) {
|
|
706
|
+
heldValue.set.delete(heldValue.ref);
|
|
707
|
+
}
|
|
708
|
+
constructor(iterable) {
|
|
709
|
+
if (iterable) for (const [key, value] of iterable) this.set(key, value);
|
|
710
|
+
}
|
|
711
|
+
set(key, value) {
|
|
712
|
+
const ref = new WeakRef(key);
|
|
713
|
+
this.#weakMap.set(key, {
|
|
714
|
+
value,
|
|
715
|
+
ref
|
|
716
|
+
});
|
|
717
|
+
this.#refSet.add(ref);
|
|
718
|
+
this.#finalizationGroup.register(key, {
|
|
719
|
+
set: this.#refSet,
|
|
720
|
+
ref
|
|
721
|
+
}, ref);
|
|
722
|
+
return this;
|
|
723
|
+
}
|
|
724
|
+
get(key) {
|
|
725
|
+
return this.#weakMap.get(key)?.value;
|
|
726
|
+
}
|
|
727
|
+
delete(key) {
|
|
728
|
+
const entry = this.#weakMap.get(key);
|
|
729
|
+
if (!entry) return false;
|
|
730
|
+
this.#weakMap.delete(key);
|
|
731
|
+
this.#refSet.delete(entry.ref);
|
|
732
|
+
this.#finalizationGroup.unregister(entry.ref);
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
*[Symbol.iterator]() {
|
|
736
|
+
for (const ref of this.#refSet) {
|
|
737
|
+
const key = ref.deref();
|
|
738
|
+
if (!key) continue;
|
|
739
|
+
const entry = this.#weakMap.get(key);
|
|
740
|
+
if (entry) yield [key, entry.value];
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
entries() {
|
|
744
|
+
return this[Symbol.iterator]();
|
|
745
|
+
}
|
|
746
|
+
*keys() {
|
|
747
|
+
for (const [key] of this) yield key;
|
|
748
|
+
}
|
|
749
|
+
*values() {
|
|
750
|
+
for (const [, value] of this) yield value;
|
|
751
|
+
}
|
|
752
|
+
forEach(callback, thisArg) {
|
|
753
|
+
for (const [key, value] of this) callback.call(thisArg, value, key, this);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
//#endregion
|
|
757
|
+
//#region src/annotation/injectable.ts
|
|
758
|
+
const _InjectableRegistry = /* @__PURE__ */ new WeakMap();
|
|
759
|
+
const _InjectableDeps = new IterableWeakMap();
|
|
760
|
+
/**
|
|
761
|
+
* Mark the class as an injectable to be handled by Sapling. The class can now be
|
|
762
|
+
* be injected into other classes, as well as allow the class to inject other `@Injectable` classes.
|
|
763
|
+
*
|
|
764
|
+
* @argument deps - An optional array to define any dependencies that this class may require.
|
|
765
|
+
*/
|
|
766
|
+
function Injectable(deps = []) {
|
|
767
|
+
return function(target) {
|
|
768
|
+
_InjectableRegistry.set(target, null);
|
|
769
|
+
_InjectableDeps.set(target, deps);
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Resolves and instantiates a class along with all of it's transitive dependencies.
|
|
774
|
+
*
|
|
775
|
+
* Uses topological sort (Kahn's algorithm) to ensure that the dependency graph is created
|
|
776
|
+
* in a correct order.
|
|
777
|
+
*
|
|
778
|
+
* When `resolve` is first called (usually during controller registration),
|
|
779
|
+
* it will compute the dependency graph of all `@Injectable` classes and instantiates
|
|
780
|
+
* them in the correct order.
|
|
781
|
+
*
|
|
782
|
+
* Subsequent calls to dependencies that have already been resolved are cached, so they will
|
|
783
|
+
* re-use the created singletons instead of re-instantiation.
|
|
784
|
+
*/
|
|
785
|
+
function _resolve(ctor) {
|
|
786
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
787
|
+
const graph = /* @__PURE__ */ new Map();
|
|
788
|
+
_InjectableDeps.forEach((deps, node) => {
|
|
789
|
+
inDegree.set(node, inDegree.get(node) || 0);
|
|
790
|
+
deps.forEach((dep) => {
|
|
791
|
+
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.`);
|
|
792
|
+
inDegree.set(dep, inDegree.get(dep) || 0);
|
|
793
|
+
inDegree.set(node, inDegree.get(node) + 1);
|
|
794
|
+
if (!graph.has(dep)) graph.set(dep, []);
|
|
795
|
+
graph.get(dep).push(node);
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
const queue = [];
|
|
799
|
+
inDegree.forEach((deg, node) => {
|
|
800
|
+
if (deg === 0) queue.push(node);
|
|
801
|
+
});
|
|
802
|
+
while (queue.length) {
|
|
803
|
+
const current = queue.shift();
|
|
804
|
+
if (!_InjectableRegistry.get(current)) {
|
|
805
|
+
const instance = new current(...(_InjectableDeps.get(current) || []).map((dep) => _InjectableRegistry.get(dep)));
|
|
806
|
+
_InjectableRegistry.set(current, instance);
|
|
807
|
+
}
|
|
808
|
+
(graph.get(current) || []).forEach((neighbor) => {
|
|
809
|
+
inDegree.set(neighbor, (inDegree.get(neighbor) ?? 0) - 1);
|
|
810
|
+
if (inDegree.get(neighbor) === 0) queue.push(neighbor);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
if (!_InjectableRegistry.get(ctor)) throw new Error("Circular dependency detected or injectable not registered");
|
|
814
|
+
return _InjectableRegistry.get(ctor);
|
|
815
|
+
}
|
|
816
|
+
//#endregion
|
|
714
817
|
//#region src/annotation/controller.ts
|
|
715
818
|
const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
|
|
716
819
|
/**
|
|
@@ -722,6 +825,7 @@ const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
|
|
|
722
825
|
function Controller({ prefix = "", deps = [] } = {}) {
|
|
723
826
|
return (target) => {
|
|
724
827
|
const targetClass = target;
|
|
828
|
+
_registerControllerClass(target, prefix);
|
|
725
829
|
const router = (0, express.Router)();
|
|
726
830
|
const routes = _getRoutes(target);
|
|
727
831
|
const usedRoutes = /* @__PURE__ */ new Set();
|
|
@@ -813,7 +917,7 @@ function __decorate(decorators, target, key, desc) {
|
|
|
813
917
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
814
918
|
}
|
|
815
919
|
//#endregion
|
|
816
|
-
//#region src/middleware/default/base.ts
|
|
920
|
+
//#region src/middleware/default/error/base.ts
|
|
817
921
|
let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
|
|
818
922
|
handle(err, _request, _response, _next) {
|
|
819
923
|
console.error("[Error]", err);
|
|
@@ -823,7 +927,17 @@ let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
|
|
|
823
927
|
__decorate([Middleware()], DefaultBaseErrorMiddleware.prototype, "handle", null);
|
|
824
928
|
DefaultBaseErrorMiddleware = __decorate([MiddlewareClass()], DefaultBaseErrorMiddleware);
|
|
825
929
|
//#endregion
|
|
826
|
-
//#region src/middleware/default/
|
|
930
|
+
//#region src/middleware/default/error/parse.ts
|
|
931
|
+
let DefaultParserErrorMiddleware = class DefaultParserErrorMiddleware {
|
|
932
|
+
handle(err, _request, _response, next) {
|
|
933
|
+
if (err instanceof ParserError) return ResponseEntity.status(err.status).body({ message: err.message });
|
|
934
|
+
next(err);
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
__decorate([Middleware()], DefaultParserErrorMiddleware.prototype, "handle", null);
|
|
938
|
+
DefaultParserErrorMiddleware = __decorate([MiddlewareClass()], DefaultParserErrorMiddleware);
|
|
939
|
+
//#endregion
|
|
940
|
+
//#region src/middleware/default/error/responsestatus.ts
|
|
827
941
|
let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddleware {
|
|
828
942
|
handle(err, _request, _response, next) {
|
|
829
943
|
if (err instanceof ResponseStatusError) return ResponseEntity.status(err.status).body({ message: err.message });
|
|
@@ -833,6 +947,41 @@ let DefaultResponseStatusErrorMiddleware = class DefaultResponseStatusErrorMiddl
|
|
|
833
947
|
__decorate([Middleware()], DefaultResponseStatusErrorMiddleware.prototype, "handle", null);
|
|
834
948
|
DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultResponseStatusErrorMiddleware);
|
|
835
949
|
//#endregion
|
|
950
|
+
//#region src/middleware/default/openapi/index.ts
|
|
951
|
+
let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
|
|
952
|
+
handle(_request, _response, _next) {
|
|
953
|
+
return ResponseEntity.ok().body(_generateOpenApiSpec());
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
__decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
|
|
957
|
+
DefaultOpenApiMiddleware = __decorate([MiddlewareClass()], DefaultOpenApiMiddleware);
|
|
958
|
+
//#endregion
|
|
959
|
+
//#region src/middleware/default/swagger/index.ts
|
|
960
|
+
let Serve = class Serve {
|
|
961
|
+
constructor() {
|
|
962
|
+
this.handlers = swagger_ui_express.default.serve;
|
|
963
|
+
}
|
|
964
|
+
handle(_request, _response, _next) {
|
|
965
|
+
return this.handlers;
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
__decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
|
|
969
|
+
Serve = __decorate([MiddlewareClass()], Serve);
|
|
970
|
+
let Setup = class Setup {
|
|
971
|
+
constructor() {
|
|
972
|
+
this.handler = swagger_ui_express.default.setup(void 0, { swaggerOptions: { url: _settings.doc.openApiPath } });
|
|
973
|
+
}
|
|
974
|
+
handle(request, response, next) {
|
|
975
|
+
return this.handler(request, response, next);
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
__decorate([Middleware(_settings.doc.swaggerPath)], Setup.prototype, "handle", null);
|
|
979
|
+
Setup = __decorate([MiddlewareClass()], Setup);
|
|
980
|
+
const DefaultSwaggerMiddleware = {
|
|
981
|
+
Serve,
|
|
982
|
+
Setup
|
|
983
|
+
};
|
|
984
|
+
//#endregion
|
|
836
985
|
exports.Controller = Controller;
|
|
837
986
|
exports.DELETE = DELETE;
|
|
838
987
|
Object.defineProperty(exports, "DefaultBaseErrorMiddleware", {
|
|
@@ -841,12 +990,25 @@ Object.defineProperty(exports, "DefaultBaseErrorMiddleware", {
|
|
|
841
990
|
return DefaultBaseErrorMiddleware;
|
|
842
991
|
}
|
|
843
992
|
});
|
|
993
|
+
Object.defineProperty(exports, "DefaultOpenApiMiddleware", {
|
|
994
|
+
enumerable: true,
|
|
995
|
+
get: function() {
|
|
996
|
+
return DefaultOpenApiMiddleware;
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
Object.defineProperty(exports, "DefaultParserErrorMiddleware", {
|
|
1000
|
+
enumerable: true,
|
|
1001
|
+
get: function() {
|
|
1002
|
+
return DefaultParserErrorMiddleware;
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
844
1005
|
Object.defineProperty(exports, "DefaultResponseStatusErrorMiddleware", {
|
|
845
1006
|
enumerable: true,
|
|
846
1007
|
get: function() {
|
|
847
1008
|
return DefaultResponseStatusErrorMiddleware;
|
|
848
1009
|
}
|
|
849
1010
|
});
|
|
1011
|
+
exports.DefaultSwaggerMiddleware = DefaultSwaggerMiddleware;
|
|
850
1012
|
exports.GET = GET;
|
|
851
1013
|
exports.HEAD = HEAD;
|
|
852
1014
|
exports.Html404ErrorPage = Html404ErrorPage;
|
|
@@ -871,8 +1033,13 @@ exports._ControllerRegistry = _ControllerRegistry;
|
|
|
871
1033
|
exports._InjectableDeps = _InjectableDeps;
|
|
872
1034
|
exports._InjectableRegistry = _InjectableRegistry;
|
|
873
1035
|
exports._Route = _Route;
|
|
1036
|
+
exports._generateOpenApiSpec = _generateOpenApiSpec;
|
|
874
1037
|
exports._getRequestSchemas = _getRequestSchemas;
|
|
875
1038
|
exports._getRoutes = _getRoutes;
|
|
876
1039
|
exports._parseOrThrow = _parseOrThrow;
|
|
1040
|
+
exports._registerControllerClass = _registerControllerClass;
|
|
877
1041
|
exports._resolve = _resolve;
|
|
1042
|
+
exports._setOpenApiConfig = _setOpenApiConfig;
|
|
1043
|
+
exports._settings = _settings;
|
|
878
1044
|
exports.methodResolve = methodResolve;
|
|
1045
|
+
exports.openApiGenerator = openApiGenerator;
|