@tahminator/sapling 2.1.0 → 2.1.2
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 +59 -3
- package/dist/index.cjs +271 -156
- package/dist/index.d.cts +78 -5
- package/dist/index.d.mts +78 -5
- package/dist/index.mjs +260 -157
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,10 @@ A lightweight Express.js dependency injection & route abstraction library.
|
|
|
26
26
|
+ [Automatic Spec Generation](#automatic-spec-generation)
|
|
27
27
|
+ [Adding Documentation with Decorators](#adding-documentation-with-decorators)
|
|
28
28
|
+ [Customizing Paths](#customizing-paths)
|
|
29
|
+
* [Health Checks](#health-checks)
|
|
30
|
+
+ [Liveness and Readiness Probes](#liveness-and-readiness-probes)
|
|
31
|
+
+ [Registering Custom Readiness Checks](#registering-custom-readiness-checks)
|
|
32
|
+
+ [Customizing Health Paths](#customizing-health-paths)
|
|
29
33
|
* [Custom Serialization](#custom-serialization)
|
|
30
34
|
- [Advanced Setup](#advanced-setup)
|
|
31
35
|
* [Automatically import controllers](#automatically-import-controllers)
|
|
@@ -97,9 +101,10 @@ class UserController {
|
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
// registerApp returns the app back to you - use the returned app instance
|
|
105
|
+
// so Sapling can inject some post-startup hooks into the app.
|
|
106
|
+
// BUT, you still have full access of app to do whatever you want!
|
|
107
|
+
const app = Sapling.registerApp(express());
|
|
103
108
|
|
|
104
109
|
// @MiddlewareClass should be registered first before @Controller and should be registered in order
|
|
105
110
|
// @Injectable classes will automatically be formed into singletons by Sapling behind the scenes!
|
|
@@ -581,6 +586,57 @@ Sapling.Extras.swaggerAndOpenApi.setOpenApiPath("/api-spec.json");
|
|
|
581
586
|
Sapling.Extras.swaggerAndOpenApi.setSwaggerPath("/api-docs");
|
|
582
587
|
```
|
|
583
588
|
|
|
589
|
+
### Health Checks
|
|
590
|
+
|
|
591
|
+
#### Liveness and Readiness Probes
|
|
592
|
+
|
|
593
|
+
Register `DefaultHealthMiddleware` to expose two health endpoints:
|
|
594
|
+
|
|
595
|
+
- `GET /live` — **liveness probe**: returns `{ up: true }` once the server has started listening. Use this to tell your orchestrator the process is alive.
|
|
596
|
+
- `GET /ready` — **readiness probe**: returns `{ up: true }` once the server is live *and* all registered readiness checks pass. Use this to gate traffic until your app is fully initialized.
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
import { Sapling, DefaultHealthMiddleware } from "@tahminator/sapling";
|
|
600
|
+
|
|
601
|
+
const app = Sapling.registerApp(express());
|
|
602
|
+
|
|
603
|
+
app.use(Sapling.resolve(DefaultHealthMiddleware));
|
|
604
|
+
|
|
605
|
+
app.listen(3000); // _markLive() is called automatically once the server is listening
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
> [!IMPORTANT]
|
|
609
|
+
> `Sapling.registerApp` returns `express.App` - please use that as your `app` object inside of your server. There is a small post-startup hook applied via a proxy, in which the liveness probe is enabled from such.
|
|
610
|
+
|
|
611
|
+
#### Registering Custom Readiness Checks
|
|
612
|
+
|
|
613
|
+
Inject `HealthRegistrar` into any `@Injectable` or controller to add readiness checks. A check is any function returning `boolean | Promise<boolean>`. If any check returns `false` (or throws), `/ready` reports `{ up: false }`.
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
import { Injectable, HealthRegistrar } from "@tahminator/sapling";
|
|
617
|
+
|
|
618
|
+
@Injectable([HealthRegistrar])
|
|
619
|
+
class DatabaseService {
|
|
620
|
+
constructor(
|
|
621
|
+
private readonly db: Database,
|
|
622
|
+
private readonly healthRegistrar: HealthRegistrar,
|
|
623
|
+
) {
|
|
624
|
+
healthRegistrar.add(async () => {
|
|
625
|
+
return this.db.isConnected();
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
#### Customizing Health Paths
|
|
632
|
+
|
|
633
|
+
The default paths are `/live` and `/ready`. Override them before registering `DefaultHealthMiddleware`:
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
Sapling.Extras.health.setLivePath("/healthz/live");
|
|
637
|
+
Sapling.Extras.health.setReadyPath("/healthz/ready");
|
|
638
|
+
```
|
|
639
|
+
|
|
584
640
|
### Custom Serialization
|
|
585
641
|
|
|
586
642
|
By default, Sapling uses `JSON.stringify` and `JSON.parse` for serialization. You can override these with custom serializers like [superjson](https://github.com/flightcontrolhq/superjson#readme) to automatically handle Dates, BigInts, and more:
|
package/dist/index.cjs
CHANGED
|
@@ -270,10 +270,181 @@ var ParserError = class ParserError extends ResponseStatusError {
|
|
|
270
270
|
}
|
|
271
271
|
};
|
|
272
272
|
//#endregion
|
|
273
|
+
//#region lib/weakmap.ts
|
|
274
|
+
/**
|
|
275
|
+
* WeakMap that is iterable.
|
|
276
|
+
*/
|
|
277
|
+
var IterableWeakMap = class IterableWeakMap {
|
|
278
|
+
#weakMap = /* @__PURE__ */ new WeakMap();
|
|
279
|
+
#refSet = /* @__PURE__ */ new Set();
|
|
280
|
+
#finalizationGroup = new FinalizationRegistry(IterableWeakMap.#cleanup);
|
|
281
|
+
static #cleanup(heldValue) {
|
|
282
|
+
heldValue.set.delete(heldValue.ref);
|
|
283
|
+
}
|
|
284
|
+
constructor(iterable) {
|
|
285
|
+
if (iterable) for (const [key, value] of iterable) this.set(key, value);
|
|
286
|
+
}
|
|
287
|
+
set(key, value) {
|
|
288
|
+
const ref = new WeakRef(key);
|
|
289
|
+
this.#weakMap.set(key, {
|
|
290
|
+
value,
|
|
291
|
+
ref
|
|
292
|
+
});
|
|
293
|
+
this.#refSet.add(ref);
|
|
294
|
+
this.#finalizationGroup.register(key, {
|
|
295
|
+
set: this.#refSet,
|
|
296
|
+
ref
|
|
297
|
+
}, ref);
|
|
298
|
+
return this;
|
|
299
|
+
}
|
|
300
|
+
get(key) {
|
|
301
|
+
return this.#weakMap.get(key)?.value;
|
|
302
|
+
}
|
|
303
|
+
delete(key) {
|
|
304
|
+
const entry = this.#weakMap.get(key);
|
|
305
|
+
if (!entry) return false;
|
|
306
|
+
this.#weakMap.delete(key);
|
|
307
|
+
this.#refSet.delete(entry.ref);
|
|
308
|
+
this.#finalizationGroup.unregister(entry.ref);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
*[Symbol.iterator]() {
|
|
312
|
+
for (const ref of this.#refSet) {
|
|
313
|
+
const key = ref.deref();
|
|
314
|
+
if (!key) continue;
|
|
315
|
+
const entry = this.#weakMap.get(key);
|
|
316
|
+
if (entry) yield [key, entry.value];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
entries() {
|
|
320
|
+
return this[Symbol.iterator]();
|
|
321
|
+
}
|
|
322
|
+
*keys() {
|
|
323
|
+
for (const [key] of this) yield key;
|
|
324
|
+
}
|
|
325
|
+
*values() {
|
|
326
|
+
for (const [, value] of this) yield value;
|
|
327
|
+
}
|
|
328
|
+
forEach(callback, thisArg) {
|
|
329
|
+
for (const [key, value] of this) callback.call(thisArg, value, key, this);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/annotation/injectable.ts
|
|
334
|
+
const _InjectableRegistry = /* @__PURE__ */ new WeakMap();
|
|
335
|
+
const _InjectableDeps = new IterableWeakMap();
|
|
336
|
+
/**
|
|
337
|
+
* Mark the class as an injectable to be handled by Sapling. The class can now be
|
|
338
|
+
* be injected into other classes, as well as allow the class to inject other `@Injectable` classes.
|
|
339
|
+
*
|
|
340
|
+
* @argument deps - An optional array to define any dependencies that this class may require.
|
|
341
|
+
*/
|
|
342
|
+
function Injectable(deps = []) {
|
|
343
|
+
return function(target) {
|
|
344
|
+
_InjectableRegistry.set(target, null);
|
|
345
|
+
_InjectableDeps.set(target, deps);
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Resolves and instantiates a class along with all of it's transitive dependencies.
|
|
350
|
+
*
|
|
351
|
+
* Uses topological sort (Kahn's algorithm) to ensure that the dependency graph is created
|
|
352
|
+
* in a correct order.
|
|
353
|
+
*
|
|
354
|
+
* When `resolve` is first called (usually during controller registration),
|
|
355
|
+
* it will compute the dependency graph of all `@Injectable` classes and instantiates
|
|
356
|
+
* them in the correct order.
|
|
357
|
+
*
|
|
358
|
+
* Subsequent calls to dependencies that have already been resolved are cached, so they will
|
|
359
|
+
* re-use the created singletons instead of re-instantiation.
|
|
360
|
+
*/
|
|
361
|
+
function _resolve(ctor) {
|
|
362
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
363
|
+
const graph = /* @__PURE__ */ new Map();
|
|
364
|
+
_InjectableDeps.forEach((deps, node) => {
|
|
365
|
+
inDegree.set(node, inDegree.get(node) || 0);
|
|
366
|
+
deps.forEach((dep) => {
|
|
367
|
+
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.`);
|
|
368
|
+
inDegree.set(dep, inDegree.get(dep) || 0);
|
|
369
|
+
inDegree.set(node, inDegree.get(node) + 1);
|
|
370
|
+
if (!graph.has(dep)) graph.set(dep, []);
|
|
371
|
+
graph.get(dep).push(node);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
const queue = [];
|
|
375
|
+
inDegree.forEach((deg, node) => {
|
|
376
|
+
if (deg === 0) queue.push(node);
|
|
377
|
+
});
|
|
378
|
+
while (queue.length) {
|
|
379
|
+
const current = queue.shift();
|
|
380
|
+
if (!_InjectableRegistry.get(current)) {
|
|
381
|
+
const instance = new current(...(_InjectableDeps.get(current) || []).map((dep) => _InjectableRegistry.get(dep)));
|
|
382
|
+
_InjectableRegistry.set(current, instance);
|
|
383
|
+
}
|
|
384
|
+
(graph.get(current) || []).forEach((neighbor) => {
|
|
385
|
+
inDegree.set(neighbor, (inDegree.get(neighbor) ?? 0) - 1);
|
|
386
|
+
if (inDegree.get(neighbor) === 0) queue.push(neighbor);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (!_InjectableRegistry.get(ctor)) throw new Error("Circular dependency detected or injectable not registered");
|
|
390
|
+
return _InjectableRegistry.get(ctor);
|
|
391
|
+
}
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region \0@oxc-project+runtime@0.127.0/helpers/decorate.js
|
|
394
|
+
function __decorate(decorators, target, key, desc) {
|
|
395
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
396
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
397
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
398
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
399
|
+
}
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/middleware/health/registrar.ts
|
|
402
|
+
let HealthRegistrar = class HealthRegistrar {
|
|
403
|
+
_checks;
|
|
404
|
+
_live;
|
|
405
|
+
constructor() {
|
|
406
|
+
this._checks = [];
|
|
407
|
+
this._live = false;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Add a health check.
|
|
411
|
+
*
|
|
412
|
+
* Health checks will be used to determine whether service can serve traffic or not (a.k.a `readiness`).
|
|
413
|
+
*/
|
|
414
|
+
add(healthCheck) {
|
|
415
|
+
this._checks.push(healthCheck);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* @internal used by Sapling library, used to determine once all
|
|
419
|
+
* checks have been registered and server is, at the very least, alive.
|
|
420
|
+
*/
|
|
421
|
+
_markLive() {
|
|
422
|
+
this._live = true;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* @internal
|
|
426
|
+
*/
|
|
427
|
+
async _liveness() {
|
|
428
|
+
return this._live;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* @internal
|
|
432
|
+
*/
|
|
433
|
+
async _readiness() {
|
|
434
|
+
if (!this._live) return false;
|
|
435
|
+
return (await Promise.allSettled(this._checks.map((c) => c()))).every((r) => r.status === "fulfilled" && r.value);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
HealthRegistrar = __decorate([Injectable()], HealthRegistrar);
|
|
439
|
+
//#endregion
|
|
273
440
|
//#region src/helper/sapling.ts
|
|
274
441
|
const _settings = {
|
|
275
442
|
serialize: JSON.stringify,
|
|
276
443
|
deserialize: JSON.parse,
|
|
444
|
+
health: {
|
|
445
|
+
ready: { path: "/readyz" },
|
|
446
|
+
live: { path: "/livez" }
|
|
447
|
+
},
|
|
277
448
|
doc: {
|
|
278
449
|
openApiPath: "/openapi.json",
|
|
279
450
|
swaggerPath: "/swagger.html",
|
|
@@ -341,14 +512,34 @@ var Sapling = class Sapling {
|
|
|
341
512
|
* import { Sapling } from "@tahminator/sapling";
|
|
342
513
|
* import express from "express";
|
|
343
514
|
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
* app.registerApp(app);
|
|
515
|
+
* // returns the exact same `express.App` type back to you!
|
|
516
|
+
* const app = Sapling.registerApp(express());
|
|
347
517
|
* ```
|
|
348
518
|
*/
|
|
349
519
|
static registerApp(app) {
|
|
350
520
|
app.use(express.default.text({ type: "application/json" }));
|
|
351
521
|
app.use(Sapling.json());
|
|
522
|
+
return new Proxy(app, { get(target, prop, receiver) {
|
|
523
|
+
if (prop === "listen") {
|
|
524
|
+
const originalListen = target[prop];
|
|
525
|
+
return function(...args) {
|
|
526
|
+
const server = originalListen.apply(target, args);
|
|
527
|
+
server.once("listening", () => {
|
|
528
|
+
Sapling._onPostStartup();
|
|
529
|
+
console.log("Sapling successfully initialized post-startup hooks on server start");
|
|
530
|
+
});
|
|
531
|
+
return server;
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
return Reflect.get(target, prop, receiver);
|
|
535
|
+
} });
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* @internal
|
|
539
|
+
* visible for testing
|
|
540
|
+
*/
|
|
541
|
+
static _onPostStartup() {
|
|
542
|
+
_InjectableRegistry.get(HealthRegistrar)?._markLive();
|
|
352
543
|
}
|
|
353
544
|
/**
|
|
354
545
|
* Serialize a value into a JSON string.
|
|
@@ -389,37 +580,59 @@ var Sapling = class Sapling {
|
|
|
389
580
|
/**
|
|
390
581
|
* Modify extra settings
|
|
391
582
|
*/
|
|
392
|
-
static Extras = {
|
|
393
|
-
/**
|
|
394
|
-
* Modify default settings applied to OpenAPI & Swagger
|
|
395
|
-
*/
|
|
396
|
-
swaggerAndOpenApi: {
|
|
397
|
-
/**
|
|
398
|
-
* Set base OpenAPI metadata values.
|
|
399
|
-
*
|
|
400
|
-
* @default { title: "API", version: "1.0.0" }
|
|
401
|
-
*/
|
|
402
|
-
setMetadata(metadata) {
|
|
403
|
-
_settings.doc.metadata = metadata;
|
|
404
|
-
},
|
|
583
|
+
static Extras = {
|
|
405
584
|
/**
|
|
406
|
-
*
|
|
407
|
-
* Swagger will also load this endpoint on load.
|
|
408
|
-
*
|
|
409
|
-
* @default `/openapi.json`
|
|
585
|
+
* Modify default settings applied to OpenAPI & Swagger
|
|
410
586
|
*/
|
|
411
|
-
|
|
412
|
-
|
|
587
|
+
swaggerAndOpenApi: {
|
|
588
|
+
/**
|
|
589
|
+
* Set base OpenAPI metadata values.
|
|
590
|
+
*
|
|
591
|
+
* @default { title: "API", version: "1.0.0" }
|
|
592
|
+
*/
|
|
593
|
+
setMetadata(metadata) {
|
|
594
|
+
_settings.doc.metadata = metadata;
|
|
595
|
+
},
|
|
596
|
+
/**
|
|
597
|
+
* change default endpoint that will serve OpenAPI spec.
|
|
598
|
+
* Swagger will also load this endpoint on load.
|
|
599
|
+
*
|
|
600
|
+
* @default `/openapi.json`
|
|
601
|
+
*/
|
|
602
|
+
setOpenApiPath(path) {
|
|
603
|
+
_settings.doc.openApiPath = path;
|
|
604
|
+
},
|
|
605
|
+
/**
|
|
606
|
+
* change Swagger endpoint.
|
|
607
|
+
*
|
|
608
|
+
* @default `/swagger.html`
|
|
609
|
+
*/
|
|
610
|
+
setSwaggerPath(path) {
|
|
611
|
+
_settings.doc.swaggerPath = path;
|
|
612
|
+
}
|
|
413
613
|
},
|
|
414
614
|
/**
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* @default `/swagger.html`
|
|
615
|
+
* Modify default settings applied to health / readiness / liveness.
|
|
418
616
|
*/
|
|
419
|
-
|
|
420
|
-
|
|
617
|
+
health: {
|
|
618
|
+
/**
|
|
619
|
+
* change default endpoint that ready endpoint will be served on.
|
|
620
|
+
*
|
|
621
|
+
* @default `/readyz`
|
|
622
|
+
*/
|
|
623
|
+
setReadyPath(path) {
|
|
624
|
+
_settings.health.ready.path = path;
|
|
625
|
+
},
|
|
626
|
+
/**
|
|
627
|
+
* change default endpoint that live endpoint will be served on.
|
|
628
|
+
*
|
|
629
|
+
* @default `/livez`
|
|
630
|
+
*/
|
|
631
|
+
setLivePath(path) {
|
|
632
|
+
_settings.health.live.path = path;
|
|
633
|
+
}
|
|
421
634
|
}
|
|
422
|
-
}
|
|
635
|
+
};
|
|
423
636
|
/**
|
|
424
637
|
* This method can be used in a `@MiddlewareClass` to register any libraries
|
|
425
638
|
* that expect you to register multiple registers at once. An example is `swagger-ui-express`
|
|
@@ -774,126 +987,6 @@ const methodResolve = {
|
|
|
774
987
|
USE: "use"
|
|
775
988
|
};
|
|
776
989
|
//#endregion
|
|
777
|
-
//#region lib/weakmap.ts
|
|
778
|
-
/**
|
|
779
|
-
* WeakMap that is iterable.
|
|
780
|
-
*/
|
|
781
|
-
var IterableWeakMap = class IterableWeakMap {
|
|
782
|
-
#weakMap = /* @__PURE__ */ new WeakMap();
|
|
783
|
-
#refSet = /* @__PURE__ */ new Set();
|
|
784
|
-
#finalizationGroup = new FinalizationRegistry(IterableWeakMap.#cleanup);
|
|
785
|
-
static #cleanup(heldValue) {
|
|
786
|
-
heldValue.set.delete(heldValue.ref);
|
|
787
|
-
}
|
|
788
|
-
constructor(iterable) {
|
|
789
|
-
if (iterable) for (const [key, value] of iterable) this.set(key, value);
|
|
790
|
-
}
|
|
791
|
-
set(key, value) {
|
|
792
|
-
const ref = new WeakRef(key);
|
|
793
|
-
this.#weakMap.set(key, {
|
|
794
|
-
value,
|
|
795
|
-
ref
|
|
796
|
-
});
|
|
797
|
-
this.#refSet.add(ref);
|
|
798
|
-
this.#finalizationGroup.register(key, {
|
|
799
|
-
set: this.#refSet,
|
|
800
|
-
ref
|
|
801
|
-
}, ref);
|
|
802
|
-
return this;
|
|
803
|
-
}
|
|
804
|
-
get(key) {
|
|
805
|
-
return this.#weakMap.get(key)?.value;
|
|
806
|
-
}
|
|
807
|
-
delete(key) {
|
|
808
|
-
const entry = this.#weakMap.get(key);
|
|
809
|
-
if (!entry) return false;
|
|
810
|
-
this.#weakMap.delete(key);
|
|
811
|
-
this.#refSet.delete(entry.ref);
|
|
812
|
-
this.#finalizationGroup.unregister(entry.ref);
|
|
813
|
-
return true;
|
|
814
|
-
}
|
|
815
|
-
*[Symbol.iterator]() {
|
|
816
|
-
for (const ref of this.#refSet) {
|
|
817
|
-
const key = ref.deref();
|
|
818
|
-
if (!key) continue;
|
|
819
|
-
const entry = this.#weakMap.get(key);
|
|
820
|
-
if (entry) yield [key, entry.value];
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
entries() {
|
|
824
|
-
return this[Symbol.iterator]();
|
|
825
|
-
}
|
|
826
|
-
*keys() {
|
|
827
|
-
for (const [key] of this) yield key;
|
|
828
|
-
}
|
|
829
|
-
*values() {
|
|
830
|
-
for (const [, value] of this) yield value;
|
|
831
|
-
}
|
|
832
|
-
forEach(callback, thisArg) {
|
|
833
|
-
for (const [key, value] of this) callback.call(thisArg, value, key, this);
|
|
834
|
-
}
|
|
835
|
-
};
|
|
836
|
-
//#endregion
|
|
837
|
-
//#region src/annotation/injectable.ts
|
|
838
|
-
const _InjectableRegistry = /* @__PURE__ */ new WeakMap();
|
|
839
|
-
const _InjectableDeps = new IterableWeakMap();
|
|
840
|
-
/**
|
|
841
|
-
* Mark the class as an injectable to be handled by Sapling. The class can now be
|
|
842
|
-
* be injected into other classes, as well as allow the class to inject other `@Injectable` classes.
|
|
843
|
-
*
|
|
844
|
-
* @argument deps - An optional array to define any dependencies that this class may require.
|
|
845
|
-
*/
|
|
846
|
-
function Injectable(deps = []) {
|
|
847
|
-
return function(target) {
|
|
848
|
-
_InjectableRegistry.set(target, null);
|
|
849
|
-
_InjectableDeps.set(target, deps);
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
/**
|
|
853
|
-
* Resolves and instantiates a class along with all of it's transitive dependencies.
|
|
854
|
-
*
|
|
855
|
-
* Uses topological sort (Kahn's algorithm) to ensure that the dependency graph is created
|
|
856
|
-
* in a correct order.
|
|
857
|
-
*
|
|
858
|
-
* When `resolve` is first called (usually during controller registration),
|
|
859
|
-
* it will compute the dependency graph of all `@Injectable` classes and instantiates
|
|
860
|
-
* them in the correct order.
|
|
861
|
-
*
|
|
862
|
-
* Subsequent calls to dependencies that have already been resolved are cached, so they will
|
|
863
|
-
* re-use the created singletons instead of re-instantiation.
|
|
864
|
-
*/
|
|
865
|
-
function _resolve(ctor) {
|
|
866
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
867
|
-
const graph = /* @__PURE__ */ new Map();
|
|
868
|
-
_InjectableDeps.forEach((deps, node) => {
|
|
869
|
-
inDegree.set(node, inDegree.get(node) || 0);
|
|
870
|
-
deps.forEach((dep) => {
|
|
871
|
-
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.`);
|
|
872
|
-
inDegree.set(dep, inDegree.get(dep) || 0);
|
|
873
|
-
inDegree.set(node, inDegree.get(node) + 1);
|
|
874
|
-
if (!graph.has(dep)) graph.set(dep, []);
|
|
875
|
-
graph.get(dep).push(node);
|
|
876
|
-
});
|
|
877
|
-
});
|
|
878
|
-
const queue = [];
|
|
879
|
-
inDegree.forEach((deg, node) => {
|
|
880
|
-
if (deg === 0) queue.push(node);
|
|
881
|
-
});
|
|
882
|
-
while (queue.length) {
|
|
883
|
-
const current = queue.shift();
|
|
884
|
-
if (!_InjectableRegistry.get(current)) {
|
|
885
|
-
const instance = new current(...(_InjectableDeps.get(current) || []).map((dep) => _InjectableRegistry.get(dep)));
|
|
886
|
-
_InjectableRegistry.set(current, instance);
|
|
887
|
-
}
|
|
888
|
-
(graph.get(current) || []).forEach((neighbor) => {
|
|
889
|
-
inDegree.set(neighbor, (inDegree.get(neighbor) ?? 0) - 1);
|
|
890
|
-
if (inDegree.get(neighbor) === 0) queue.push(neighbor);
|
|
891
|
-
});
|
|
892
|
-
}
|
|
893
|
-
if (!_InjectableRegistry.get(ctor)) throw new Error("Circular dependency detected or injectable not registered");
|
|
894
|
-
return _InjectableRegistry.get(ctor);
|
|
895
|
-
}
|
|
896
|
-
//#endregion
|
|
897
990
|
//#region src/annotation/controller.ts
|
|
898
991
|
const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
|
|
899
992
|
/**
|
|
@@ -1012,14 +1105,6 @@ function MiddlewareClass(...args) {
|
|
|
1012
1105
|
return Controller(...args);
|
|
1013
1106
|
}
|
|
1014
1107
|
//#endregion
|
|
1015
|
-
//#region \0@oxc-project+runtime@0.127.0/helpers/decorate.js
|
|
1016
|
-
function __decorate(decorators, target, key, desc) {
|
|
1017
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1018
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1019
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1020
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1021
|
-
}
|
|
1022
|
-
//#endregion
|
|
1023
1108
|
//#region src/middleware/default/error/base.ts
|
|
1024
1109
|
let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
|
|
1025
1110
|
handle(err, _request, _response, _next) {
|
|
@@ -1135,6 +1220,24 @@ const DefaultSwaggerMiddleware = {
|
|
|
1135
1220
|
Setup
|
|
1136
1221
|
};
|
|
1137
1222
|
//#endregion
|
|
1223
|
+
//#region src/middleware/default/health/index.ts
|
|
1224
|
+
let DefaultHealthMiddleware = class DefaultHealthMiddleware {
|
|
1225
|
+
constructor(healthRegistrar) {
|
|
1226
|
+
this.healthRegistrar = healthRegistrar;
|
|
1227
|
+
}
|
|
1228
|
+
async readiness(_request, _response, _next) {
|
|
1229
|
+
const up = await this.healthRegistrar._readiness();
|
|
1230
|
+
return ResponseEntity.status(up ? 200 : 503).body({ up });
|
|
1231
|
+
}
|
|
1232
|
+
async liveness(_request, _response, _next) {
|
|
1233
|
+
const up = await this.healthRegistrar._liveness();
|
|
1234
|
+
return ResponseEntity.status(up ? 200 : 503).body({ up });
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
__decorate([GET(_settings.health.ready.path)], DefaultHealthMiddleware.prototype, "readiness", null);
|
|
1238
|
+
__decorate([GET(_settings.health.live.path)], DefaultHealthMiddleware.prototype, "liveness", null);
|
|
1239
|
+
DefaultHealthMiddleware = __decorate([MiddlewareClass({ deps: [HealthRegistrar] })], DefaultHealthMiddleware);
|
|
1240
|
+
//#endregion
|
|
1138
1241
|
exports.Controller = Controller;
|
|
1139
1242
|
exports.ControllerSchema = ControllerSchema;
|
|
1140
1243
|
exports.DELETE = DELETE;
|
|
@@ -1144,6 +1247,12 @@ Object.defineProperty(exports, "DefaultBaseErrorMiddleware", {
|
|
|
1144
1247
|
return DefaultBaseErrorMiddleware;
|
|
1145
1248
|
}
|
|
1146
1249
|
});
|
|
1250
|
+
Object.defineProperty(exports, "DefaultHealthMiddleware", {
|
|
1251
|
+
enumerable: true,
|
|
1252
|
+
get: function() {
|
|
1253
|
+
return DefaultHealthMiddleware;
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1147
1256
|
Object.defineProperty(exports, "DefaultOpenApiMiddleware", {
|
|
1148
1257
|
enumerable: true,
|
|
1149
1258
|
get: function() {
|
|
@@ -1165,6 +1274,12 @@ Object.defineProperty(exports, "DefaultResponseStatusErrorMiddleware", {
|
|
|
1165
1274
|
exports.DefaultSwaggerMiddleware = DefaultSwaggerMiddleware;
|
|
1166
1275
|
exports.GET = GET;
|
|
1167
1276
|
exports.HEAD = HEAD;
|
|
1277
|
+
Object.defineProperty(exports, "HealthRegistrar", {
|
|
1278
|
+
enumerable: true,
|
|
1279
|
+
get: function() {
|
|
1280
|
+
return HealthRegistrar;
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1168
1283
|
exports.Html404ErrorPage = Html404ErrorPage;
|
|
1169
1284
|
exports.HttpStatus = HttpStatus;
|
|
1170
1285
|
exports.Injectable = Injectable;
|
package/dist/index.d.cts
CHANGED
|
@@ -794,6 +794,14 @@ declare function _clearOpenApiRegistry(): void;
|
|
|
794
794
|
type Settings = {
|
|
795
795
|
serialize: (value: any) => string;
|
|
796
796
|
deserialize: (value: string) => any;
|
|
797
|
+
health: {
|
|
798
|
+
ready: {
|
|
799
|
+
path: string;
|
|
800
|
+
};
|
|
801
|
+
live: {
|
|
802
|
+
path: string;
|
|
803
|
+
};
|
|
804
|
+
};
|
|
797
805
|
doc: {
|
|
798
806
|
openApiPath: string;
|
|
799
807
|
swaggerPath: string;
|
|
@@ -840,12 +848,16 @@ declare class Sapling {
|
|
|
840
848
|
* import { Sapling } from "@tahminator/sapling";
|
|
841
849
|
* import express from "express";
|
|
842
850
|
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
* app.registerApp(app);
|
|
851
|
+
* // returns the exact same `express.App` type back to you!
|
|
852
|
+
* const app = Sapling.registerApp(express());
|
|
846
853
|
* ```
|
|
847
854
|
*/
|
|
848
|
-
static registerApp(app: e.Express):
|
|
855
|
+
static registerApp(app: e.Express): e.Express;
|
|
856
|
+
/**
|
|
857
|
+
* @internal
|
|
858
|
+
* visible for testing
|
|
859
|
+
*/
|
|
860
|
+
static _onPostStartup(): void;
|
|
849
861
|
/**
|
|
850
862
|
* Serialize a value into a JSON string.
|
|
851
863
|
*
|
|
@@ -902,6 +914,23 @@ declare class Sapling {
|
|
|
902
914
|
*/
|
|
903
915
|
setSwaggerPath(this: void, path: string): void;
|
|
904
916
|
};
|
|
917
|
+
/**
|
|
918
|
+
* Modify default settings applied to health / readiness / liveness.
|
|
919
|
+
*/
|
|
920
|
+
health: {
|
|
921
|
+
/**
|
|
922
|
+
* change default endpoint that ready endpoint will be served on.
|
|
923
|
+
*
|
|
924
|
+
* @default `/readyz`
|
|
925
|
+
*/
|
|
926
|
+
setReadyPath(this: void, path: string): void;
|
|
927
|
+
/**
|
|
928
|
+
* change default endpoint that live endpoint will be served on.
|
|
929
|
+
*
|
|
930
|
+
* @default `/livez`
|
|
931
|
+
*/
|
|
932
|
+
setLivePath(this: void, path: string): void;
|
|
933
|
+
};
|
|
905
934
|
};
|
|
906
935
|
/**
|
|
907
936
|
* This method can be used in a `@MiddlewareClass` to register any libraries
|
|
@@ -1089,4 +1118,48 @@ declare const DefaultSwaggerMiddleware: {
|
|
|
1089
1118
|
Setup: typeof Setup;
|
|
1090
1119
|
};
|
|
1091
1120
|
//#endregion
|
|
1092
|
-
|
|
1121
|
+
//#region src/middleware/health/registrar.d.ts
|
|
1122
|
+
type HealthCheck = () => boolean | Promise<boolean>;
|
|
1123
|
+
declare class HealthRegistrar {
|
|
1124
|
+
private _checks;
|
|
1125
|
+
private _live;
|
|
1126
|
+
constructor();
|
|
1127
|
+
/**
|
|
1128
|
+
* Add a health check.
|
|
1129
|
+
*
|
|
1130
|
+
* Health checks will be used to determine whether service can serve traffic or not (a.k.a `readiness`).
|
|
1131
|
+
*/
|
|
1132
|
+
add(healthCheck: HealthCheck): void;
|
|
1133
|
+
/**
|
|
1134
|
+
* @internal used by Sapling library, used to determine once all
|
|
1135
|
+
* checks have been registered and server is, at the very least, alive.
|
|
1136
|
+
*/
|
|
1137
|
+
_markLive(): void;
|
|
1138
|
+
/**
|
|
1139
|
+
* @internal
|
|
1140
|
+
*/
|
|
1141
|
+
_liveness(): Promise<boolean>;
|
|
1142
|
+
/**
|
|
1143
|
+
* @internal
|
|
1144
|
+
*/
|
|
1145
|
+
_readiness(): Promise<boolean>;
|
|
1146
|
+
}
|
|
1147
|
+
//#endregion
|
|
1148
|
+
//#region src/middleware/default/health/index.d.ts
|
|
1149
|
+
/**
|
|
1150
|
+
* Enable the serving of `ready` and `live` endpoints.
|
|
1151
|
+
*
|
|
1152
|
+
* Configure any middleware-specific settings with `Sapling.Extras.health`
|
|
1153
|
+
*/
|
|
1154
|
+
declare class DefaultHealthMiddleware {
|
|
1155
|
+
private readonly healthRegistrar;
|
|
1156
|
+
constructor(healthRegistrar: HealthRegistrar);
|
|
1157
|
+
readiness(_request: Request, _response: Response$1, _next: NextFunction): Promise<ResponseEntity<{
|
|
1158
|
+
up: boolean;
|
|
1159
|
+
}>>;
|
|
1160
|
+
liveness(_request: Request, _response: Response$1, _next: NextFunction): Promise<ResponseEntity<{
|
|
1161
|
+
up: boolean;
|
|
1162
|
+
}>>;
|
|
1163
|
+
}
|
|
1164
|
+
//#endregion
|
|
1165
|
+
export { Class, Controller, ControllerSchema, ControllerSchemaDefinition, DELETE, DefaultBaseErrorMiddleware, DefaultHealthMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, ExpressMiddlewareFn, ExpressRouterMethodKey, ExpressRouterMethods, GET, HEAD, HealthCheck, HealthRegistrar, Html404ErrorPage, HttpHeaders, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, OpenAPIMetadata, PATCH, POST, PUT, ParserError, ParserErrorLocation, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseBody, ResponseEntity, ResponseEntityBuilder, ResponseSchema, ResponseStatusError, RouteDefinition, RouteSchema, RouteSchemaDefinition, Sapling, ValidatorSchema, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _clearOpenApiRegistry, _getControllerSchema, _getOrCreateSchemaDefinition, _getRouteSchema, _getRoutes, _getValidatorSchema, _parseOrThrow, _registerController, _resolve, _saveValidatorSchema, _setControllerSchema, _setRouteSchema, _settings, generateOpenApiSpec, methodResolve, openApiGenerator };
|
package/dist/index.d.mts
CHANGED
|
@@ -794,6 +794,14 @@ declare function _clearOpenApiRegistry(): void;
|
|
|
794
794
|
type Settings = {
|
|
795
795
|
serialize: (value: any) => string;
|
|
796
796
|
deserialize: (value: string) => any;
|
|
797
|
+
health: {
|
|
798
|
+
ready: {
|
|
799
|
+
path: string;
|
|
800
|
+
};
|
|
801
|
+
live: {
|
|
802
|
+
path: string;
|
|
803
|
+
};
|
|
804
|
+
};
|
|
797
805
|
doc: {
|
|
798
806
|
openApiPath: string;
|
|
799
807
|
swaggerPath: string;
|
|
@@ -840,12 +848,16 @@ declare class Sapling {
|
|
|
840
848
|
* import { Sapling } from "@tahminator/sapling";
|
|
841
849
|
* import express from "express";
|
|
842
850
|
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
* app.registerApp(app);
|
|
851
|
+
* // returns the exact same `express.App` type back to you!
|
|
852
|
+
* const app = Sapling.registerApp(express());
|
|
846
853
|
* ```
|
|
847
854
|
*/
|
|
848
|
-
static registerApp(app: e.Express):
|
|
855
|
+
static registerApp(app: e.Express): e.Express;
|
|
856
|
+
/**
|
|
857
|
+
* @internal
|
|
858
|
+
* visible for testing
|
|
859
|
+
*/
|
|
860
|
+
static _onPostStartup(): void;
|
|
849
861
|
/**
|
|
850
862
|
* Serialize a value into a JSON string.
|
|
851
863
|
*
|
|
@@ -902,6 +914,23 @@ declare class Sapling {
|
|
|
902
914
|
*/
|
|
903
915
|
setSwaggerPath(this: void, path: string): void;
|
|
904
916
|
};
|
|
917
|
+
/**
|
|
918
|
+
* Modify default settings applied to health / readiness / liveness.
|
|
919
|
+
*/
|
|
920
|
+
health: {
|
|
921
|
+
/**
|
|
922
|
+
* change default endpoint that ready endpoint will be served on.
|
|
923
|
+
*
|
|
924
|
+
* @default `/readyz`
|
|
925
|
+
*/
|
|
926
|
+
setReadyPath(this: void, path: string): void;
|
|
927
|
+
/**
|
|
928
|
+
* change default endpoint that live endpoint will be served on.
|
|
929
|
+
*
|
|
930
|
+
* @default `/livez`
|
|
931
|
+
*/
|
|
932
|
+
setLivePath(this: void, path: string): void;
|
|
933
|
+
};
|
|
905
934
|
};
|
|
906
935
|
/**
|
|
907
936
|
* This method can be used in a `@MiddlewareClass` to register any libraries
|
|
@@ -1089,4 +1118,48 @@ declare const DefaultSwaggerMiddleware: {
|
|
|
1089
1118
|
Setup: typeof Setup;
|
|
1090
1119
|
};
|
|
1091
1120
|
//#endregion
|
|
1092
|
-
|
|
1121
|
+
//#region src/middleware/health/registrar.d.ts
|
|
1122
|
+
type HealthCheck = () => boolean | Promise<boolean>;
|
|
1123
|
+
declare class HealthRegistrar {
|
|
1124
|
+
private _checks;
|
|
1125
|
+
private _live;
|
|
1126
|
+
constructor();
|
|
1127
|
+
/**
|
|
1128
|
+
* Add a health check.
|
|
1129
|
+
*
|
|
1130
|
+
* Health checks will be used to determine whether service can serve traffic or not (a.k.a `readiness`).
|
|
1131
|
+
*/
|
|
1132
|
+
add(healthCheck: HealthCheck): void;
|
|
1133
|
+
/**
|
|
1134
|
+
* @internal used by Sapling library, used to determine once all
|
|
1135
|
+
* checks have been registered and server is, at the very least, alive.
|
|
1136
|
+
*/
|
|
1137
|
+
_markLive(): void;
|
|
1138
|
+
/**
|
|
1139
|
+
* @internal
|
|
1140
|
+
*/
|
|
1141
|
+
_liveness(): Promise<boolean>;
|
|
1142
|
+
/**
|
|
1143
|
+
* @internal
|
|
1144
|
+
*/
|
|
1145
|
+
_readiness(): Promise<boolean>;
|
|
1146
|
+
}
|
|
1147
|
+
//#endregion
|
|
1148
|
+
//#region src/middleware/default/health/index.d.ts
|
|
1149
|
+
/**
|
|
1150
|
+
* Enable the serving of `ready` and `live` endpoints.
|
|
1151
|
+
*
|
|
1152
|
+
* Configure any middleware-specific settings with `Sapling.Extras.health`
|
|
1153
|
+
*/
|
|
1154
|
+
declare class DefaultHealthMiddleware {
|
|
1155
|
+
private readonly healthRegistrar;
|
|
1156
|
+
constructor(healthRegistrar: HealthRegistrar);
|
|
1157
|
+
readiness(_request: Request, _response: Response$1, _next: NextFunction): Promise<ResponseEntity<{
|
|
1158
|
+
up: boolean;
|
|
1159
|
+
}>>;
|
|
1160
|
+
liveness(_request: Request, _response: Response$1, _next: NextFunction): Promise<ResponseEntity<{
|
|
1161
|
+
up: boolean;
|
|
1162
|
+
}>>;
|
|
1163
|
+
}
|
|
1164
|
+
//#endregion
|
|
1165
|
+
export { Class, Controller, ControllerSchema, ControllerSchemaDefinition, DELETE, DefaultBaseErrorMiddleware, DefaultHealthMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, ExpressMiddlewareFn, ExpressRouterMethodKey, ExpressRouterMethods, GET, HEAD, HealthCheck, HealthRegistrar, Html404ErrorPage, HttpHeaders, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, OpenAPIMetadata, PATCH, POST, PUT, ParserError, ParserErrorLocation, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseBody, ResponseEntity, ResponseEntityBuilder, ResponseSchema, ResponseStatusError, RouteDefinition, RouteSchema, RouteSchemaDefinition, Sapling, ValidatorSchema, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _clearOpenApiRegistry, _getControllerSchema, _getOrCreateSchemaDefinition, _getRouteSchema, _getRoutes, _getValidatorSchema, _parseOrThrow, _registerController, _resolve, _saveValidatorSchema, _setControllerSchema, _setRouteSchema, _settings, generateOpenApiSpec, methodResolve, openApiGenerator };
|
package/dist/index.mjs
CHANGED
|
@@ -245,10 +245,181 @@ var ParserError = class ParserError extends ResponseStatusError {
|
|
|
245
245
|
}
|
|
246
246
|
};
|
|
247
247
|
//#endregion
|
|
248
|
+
//#region lib/weakmap.ts
|
|
249
|
+
/**
|
|
250
|
+
* WeakMap that is iterable.
|
|
251
|
+
*/
|
|
252
|
+
var IterableWeakMap = class IterableWeakMap {
|
|
253
|
+
#weakMap = /* @__PURE__ */ new WeakMap();
|
|
254
|
+
#refSet = /* @__PURE__ */ new Set();
|
|
255
|
+
#finalizationGroup = new FinalizationRegistry(IterableWeakMap.#cleanup);
|
|
256
|
+
static #cleanup(heldValue) {
|
|
257
|
+
heldValue.set.delete(heldValue.ref);
|
|
258
|
+
}
|
|
259
|
+
constructor(iterable) {
|
|
260
|
+
if (iterable) for (const [key, value] of iterable) this.set(key, value);
|
|
261
|
+
}
|
|
262
|
+
set(key, value) {
|
|
263
|
+
const ref = new WeakRef(key);
|
|
264
|
+
this.#weakMap.set(key, {
|
|
265
|
+
value,
|
|
266
|
+
ref
|
|
267
|
+
});
|
|
268
|
+
this.#refSet.add(ref);
|
|
269
|
+
this.#finalizationGroup.register(key, {
|
|
270
|
+
set: this.#refSet,
|
|
271
|
+
ref
|
|
272
|
+
}, ref);
|
|
273
|
+
return this;
|
|
274
|
+
}
|
|
275
|
+
get(key) {
|
|
276
|
+
return this.#weakMap.get(key)?.value;
|
|
277
|
+
}
|
|
278
|
+
delete(key) {
|
|
279
|
+
const entry = this.#weakMap.get(key);
|
|
280
|
+
if (!entry) return false;
|
|
281
|
+
this.#weakMap.delete(key);
|
|
282
|
+
this.#refSet.delete(entry.ref);
|
|
283
|
+
this.#finalizationGroup.unregister(entry.ref);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
*[Symbol.iterator]() {
|
|
287
|
+
for (const ref of this.#refSet) {
|
|
288
|
+
const key = ref.deref();
|
|
289
|
+
if (!key) continue;
|
|
290
|
+
const entry = this.#weakMap.get(key);
|
|
291
|
+
if (entry) yield [key, entry.value];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
entries() {
|
|
295
|
+
return this[Symbol.iterator]();
|
|
296
|
+
}
|
|
297
|
+
*keys() {
|
|
298
|
+
for (const [key] of this) yield key;
|
|
299
|
+
}
|
|
300
|
+
*values() {
|
|
301
|
+
for (const [, value] of this) yield value;
|
|
302
|
+
}
|
|
303
|
+
forEach(callback, thisArg) {
|
|
304
|
+
for (const [key, value] of this) callback.call(thisArg, value, key, this);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/annotation/injectable.ts
|
|
309
|
+
const _InjectableRegistry = /* @__PURE__ */ new WeakMap();
|
|
310
|
+
const _InjectableDeps = new IterableWeakMap();
|
|
311
|
+
/**
|
|
312
|
+
* Mark the class as an injectable to be handled by Sapling. The class can now be
|
|
313
|
+
* be injected into other classes, as well as allow the class to inject other `@Injectable` classes.
|
|
314
|
+
*
|
|
315
|
+
* @argument deps - An optional array to define any dependencies that this class may require.
|
|
316
|
+
*/
|
|
317
|
+
function Injectable(deps = []) {
|
|
318
|
+
return function(target) {
|
|
319
|
+
_InjectableRegistry.set(target, null);
|
|
320
|
+
_InjectableDeps.set(target, deps);
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Resolves and instantiates a class along with all of it's transitive dependencies.
|
|
325
|
+
*
|
|
326
|
+
* Uses topological sort (Kahn's algorithm) to ensure that the dependency graph is created
|
|
327
|
+
* in a correct order.
|
|
328
|
+
*
|
|
329
|
+
* When `resolve` is first called (usually during controller registration),
|
|
330
|
+
* it will compute the dependency graph of all `@Injectable` classes and instantiates
|
|
331
|
+
* them in the correct order.
|
|
332
|
+
*
|
|
333
|
+
* Subsequent calls to dependencies that have already been resolved are cached, so they will
|
|
334
|
+
* re-use the created singletons instead of re-instantiation.
|
|
335
|
+
*/
|
|
336
|
+
function _resolve(ctor) {
|
|
337
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
338
|
+
const graph = /* @__PURE__ */ new Map();
|
|
339
|
+
_InjectableDeps.forEach((deps, node) => {
|
|
340
|
+
inDegree.set(node, inDegree.get(node) || 0);
|
|
341
|
+
deps.forEach((dep) => {
|
|
342
|
+
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.`);
|
|
343
|
+
inDegree.set(dep, inDegree.get(dep) || 0);
|
|
344
|
+
inDegree.set(node, inDegree.get(node) + 1);
|
|
345
|
+
if (!graph.has(dep)) graph.set(dep, []);
|
|
346
|
+
graph.get(dep).push(node);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
const queue = [];
|
|
350
|
+
inDegree.forEach((deg, node) => {
|
|
351
|
+
if (deg === 0) queue.push(node);
|
|
352
|
+
});
|
|
353
|
+
while (queue.length) {
|
|
354
|
+
const current = queue.shift();
|
|
355
|
+
if (!_InjectableRegistry.get(current)) {
|
|
356
|
+
const instance = new current(...(_InjectableDeps.get(current) || []).map((dep) => _InjectableRegistry.get(dep)));
|
|
357
|
+
_InjectableRegistry.set(current, instance);
|
|
358
|
+
}
|
|
359
|
+
(graph.get(current) || []).forEach((neighbor) => {
|
|
360
|
+
inDegree.set(neighbor, (inDegree.get(neighbor) ?? 0) - 1);
|
|
361
|
+
if (inDegree.get(neighbor) === 0) queue.push(neighbor);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (!_InjectableRegistry.get(ctor)) throw new Error("Circular dependency detected or injectable not registered");
|
|
365
|
+
return _InjectableRegistry.get(ctor);
|
|
366
|
+
}
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region \0@oxc-project+runtime@0.127.0/helpers/decorate.js
|
|
369
|
+
function __decorate(decorators, target, key, desc) {
|
|
370
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
371
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
372
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
373
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
374
|
+
}
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/middleware/health/registrar.ts
|
|
377
|
+
let HealthRegistrar = class HealthRegistrar {
|
|
378
|
+
_checks;
|
|
379
|
+
_live;
|
|
380
|
+
constructor() {
|
|
381
|
+
this._checks = [];
|
|
382
|
+
this._live = false;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Add a health check.
|
|
386
|
+
*
|
|
387
|
+
* Health checks will be used to determine whether service can serve traffic or not (a.k.a `readiness`).
|
|
388
|
+
*/
|
|
389
|
+
add(healthCheck) {
|
|
390
|
+
this._checks.push(healthCheck);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* @internal used by Sapling library, used to determine once all
|
|
394
|
+
* checks have been registered and server is, at the very least, alive.
|
|
395
|
+
*/
|
|
396
|
+
_markLive() {
|
|
397
|
+
this._live = true;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* @internal
|
|
401
|
+
*/
|
|
402
|
+
async _liveness() {
|
|
403
|
+
return this._live;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* @internal
|
|
407
|
+
*/
|
|
408
|
+
async _readiness() {
|
|
409
|
+
if (!this._live) return false;
|
|
410
|
+
return (await Promise.allSettled(this._checks.map((c) => c()))).every((r) => r.status === "fulfilled" && r.value);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
HealthRegistrar = __decorate([Injectable()], HealthRegistrar);
|
|
414
|
+
//#endregion
|
|
248
415
|
//#region src/helper/sapling.ts
|
|
249
416
|
const _settings = {
|
|
250
417
|
serialize: JSON.stringify,
|
|
251
418
|
deserialize: JSON.parse,
|
|
419
|
+
health: {
|
|
420
|
+
ready: { path: "/readyz" },
|
|
421
|
+
live: { path: "/livez" }
|
|
422
|
+
},
|
|
252
423
|
doc: {
|
|
253
424
|
openApiPath: "/openapi.json",
|
|
254
425
|
swaggerPath: "/swagger.html",
|
|
@@ -316,14 +487,34 @@ var Sapling = class Sapling {
|
|
|
316
487
|
* import { Sapling } from "@tahminator/sapling";
|
|
317
488
|
* import express from "express";
|
|
318
489
|
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
* app.registerApp(app);
|
|
490
|
+
* // returns the exact same `express.App` type back to you!
|
|
491
|
+
* const app = Sapling.registerApp(express());
|
|
322
492
|
* ```
|
|
323
493
|
*/
|
|
324
494
|
static registerApp(app) {
|
|
325
495
|
app.use(e.text({ type: "application/json" }));
|
|
326
496
|
app.use(Sapling.json());
|
|
497
|
+
return new Proxy(app, { get(target, prop, receiver) {
|
|
498
|
+
if (prop === "listen") {
|
|
499
|
+
const originalListen = target[prop];
|
|
500
|
+
return function(...args) {
|
|
501
|
+
const server = originalListen.apply(target, args);
|
|
502
|
+
server.once("listening", () => {
|
|
503
|
+
Sapling._onPostStartup();
|
|
504
|
+
console.log("Sapling successfully initialized post-startup hooks on server start");
|
|
505
|
+
});
|
|
506
|
+
return server;
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
return Reflect.get(target, prop, receiver);
|
|
510
|
+
} });
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* @internal
|
|
514
|
+
* visible for testing
|
|
515
|
+
*/
|
|
516
|
+
static _onPostStartup() {
|
|
517
|
+
_InjectableRegistry.get(HealthRegistrar)?._markLive();
|
|
327
518
|
}
|
|
328
519
|
/**
|
|
329
520
|
* Serialize a value into a JSON string.
|
|
@@ -364,37 +555,59 @@ var Sapling = class Sapling {
|
|
|
364
555
|
/**
|
|
365
556
|
* Modify extra settings
|
|
366
557
|
*/
|
|
367
|
-
static Extras = {
|
|
368
|
-
/**
|
|
369
|
-
* Modify default settings applied to OpenAPI & Swagger
|
|
370
|
-
*/
|
|
371
|
-
swaggerAndOpenApi: {
|
|
558
|
+
static Extras = {
|
|
372
559
|
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* @default { title: "API", version: "1.0.0" }
|
|
560
|
+
* Modify default settings applied to OpenAPI & Swagger
|
|
376
561
|
*/
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
562
|
+
swaggerAndOpenApi: {
|
|
563
|
+
/**
|
|
564
|
+
* Set base OpenAPI metadata values.
|
|
565
|
+
*
|
|
566
|
+
* @default { title: "API", version: "1.0.0" }
|
|
567
|
+
*/
|
|
568
|
+
setMetadata(metadata) {
|
|
569
|
+
_settings.doc.metadata = metadata;
|
|
570
|
+
},
|
|
571
|
+
/**
|
|
572
|
+
* change default endpoint that will serve OpenAPI spec.
|
|
573
|
+
* Swagger will also load this endpoint on load.
|
|
574
|
+
*
|
|
575
|
+
* @default `/openapi.json`
|
|
576
|
+
*/
|
|
577
|
+
setOpenApiPath(path) {
|
|
578
|
+
_settings.doc.openApiPath = path;
|
|
579
|
+
},
|
|
580
|
+
/**
|
|
581
|
+
* change Swagger endpoint.
|
|
582
|
+
*
|
|
583
|
+
* @default `/swagger.html`
|
|
584
|
+
*/
|
|
585
|
+
setSwaggerPath(path) {
|
|
586
|
+
_settings.doc.swaggerPath = path;
|
|
587
|
+
}
|
|
388
588
|
},
|
|
389
589
|
/**
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* @default `/swagger.html`
|
|
590
|
+
* Modify default settings applied to health / readiness / liveness.
|
|
393
591
|
*/
|
|
394
|
-
|
|
395
|
-
|
|
592
|
+
health: {
|
|
593
|
+
/**
|
|
594
|
+
* change default endpoint that ready endpoint will be served on.
|
|
595
|
+
*
|
|
596
|
+
* @default `/readyz`
|
|
597
|
+
*/
|
|
598
|
+
setReadyPath(path) {
|
|
599
|
+
_settings.health.ready.path = path;
|
|
600
|
+
},
|
|
601
|
+
/**
|
|
602
|
+
* change default endpoint that live endpoint will be served on.
|
|
603
|
+
*
|
|
604
|
+
* @default `/livez`
|
|
605
|
+
*/
|
|
606
|
+
setLivePath(path) {
|
|
607
|
+
_settings.health.live.path = path;
|
|
608
|
+
}
|
|
396
609
|
}
|
|
397
|
-
}
|
|
610
|
+
};
|
|
398
611
|
/**
|
|
399
612
|
* This method can be used in a `@MiddlewareClass` to register any libraries
|
|
400
613
|
* that expect you to register multiple registers at once. An example is `swagger-ui-express`
|
|
@@ -749,126 +962,6 @@ const methodResolve = {
|
|
|
749
962
|
USE: "use"
|
|
750
963
|
};
|
|
751
964
|
//#endregion
|
|
752
|
-
//#region lib/weakmap.ts
|
|
753
|
-
/**
|
|
754
|
-
* WeakMap that is iterable.
|
|
755
|
-
*/
|
|
756
|
-
var IterableWeakMap = class IterableWeakMap {
|
|
757
|
-
#weakMap = /* @__PURE__ */ new WeakMap();
|
|
758
|
-
#refSet = /* @__PURE__ */ new Set();
|
|
759
|
-
#finalizationGroup = new FinalizationRegistry(IterableWeakMap.#cleanup);
|
|
760
|
-
static #cleanup(heldValue) {
|
|
761
|
-
heldValue.set.delete(heldValue.ref);
|
|
762
|
-
}
|
|
763
|
-
constructor(iterable) {
|
|
764
|
-
if (iterable) for (const [key, value] of iterable) this.set(key, value);
|
|
765
|
-
}
|
|
766
|
-
set(key, value) {
|
|
767
|
-
const ref = new WeakRef(key);
|
|
768
|
-
this.#weakMap.set(key, {
|
|
769
|
-
value,
|
|
770
|
-
ref
|
|
771
|
-
});
|
|
772
|
-
this.#refSet.add(ref);
|
|
773
|
-
this.#finalizationGroup.register(key, {
|
|
774
|
-
set: this.#refSet,
|
|
775
|
-
ref
|
|
776
|
-
}, ref);
|
|
777
|
-
return this;
|
|
778
|
-
}
|
|
779
|
-
get(key) {
|
|
780
|
-
return this.#weakMap.get(key)?.value;
|
|
781
|
-
}
|
|
782
|
-
delete(key) {
|
|
783
|
-
const entry = this.#weakMap.get(key);
|
|
784
|
-
if (!entry) return false;
|
|
785
|
-
this.#weakMap.delete(key);
|
|
786
|
-
this.#refSet.delete(entry.ref);
|
|
787
|
-
this.#finalizationGroup.unregister(entry.ref);
|
|
788
|
-
return true;
|
|
789
|
-
}
|
|
790
|
-
*[Symbol.iterator]() {
|
|
791
|
-
for (const ref of this.#refSet) {
|
|
792
|
-
const key = ref.deref();
|
|
793
|
-
if (!key) continue;
|
|
794
|
-
const entry = this.#weakMap.get(key);
|
|
795
|
-
if (entry) yield [key, entry.value];
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
entries() {
|
|
799
|
-
return this[Symbol.iterator]();
|
|
800
|
-
}
|
|
801
|
-
*keys() {
|
|
802
|
-
for (const [key] of this) yield key;
|
|
803
|
-
}
|
|
804
|
-
*values() {
|
|
805
|
-
for (const [, value] of this) yield value;
|
|
806
|
-
}
|
|
807
|
-
forEach(callback, thisArg) {
|
|
808
|
-
for (const [key, value] of this) callback.call(thisArg, value, key, this);
|
|
809
|
-
}
|
|
810
|
-
};
|
|
811
|
-
//#endregion
|
|
812
|
-
//#region src/annotation/injectable.ts
|
|
813
|
-
const _InjectableRegistry = /* @__PURE__ */ new WeakMap();
|
|
814
|
-
const _InjectableDeps = new IterableWeakMap();
|
|
815
|
-
/**
|
|
816
|
-
* Mark the class as an injectable to be handled by Sapling. The class can now be
|
|
817
|
-
* be injected into other classes, as well as allow the class to inject other `@Injectable` classes.
|
|
818
|
-
*
|
|
819
|
-
* @argument deps - An optional array to define any dependencies that this class may require.
|
|
820
|
-
*/
|
|
821
|
-
function Injectable(deps = []) {
|
|
822
|
-
return function(target) {
|
|
823
|
-
_InjectableRegistry.set(target, null);
|
|
824
|
-
_InjectableDeps.set(target, deps);
|
|
825
|
-
};
|
|
826
|
-
}
|
|
827
|
-
/**
|
|
828
|
-
* Resolves and instantiates a class along with all of it's transitive dependencies.
|
|
829
|
-
*
|
|
830
|
-
* Uses topological sort (Kahn's algorithm) to ensure that the dependency graph is created
|
|
831
|
-
* in a correct order.
|
|
832
|
-
*
|
|
833
|
-
* When `resolve` is first called (usually during controller registration),
|
|
834
|
-
* it will compute the dependency graph of all `@Injectable` classes and instantiates
|
|
835
|
-
* them in the correct order.
|
|
836
|
-
*
|
|
837
|
-
* Subsequent calls to dependencies that have already been resolved are cached, so they will
|
|
838
|
-
* re-use the created singletons instead of re-instantiation.
|
|
839
|
-
*/
|
|
840
|
-
function _resolve(ctor) {
|
|
841
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
842
|
-
const graph = /* @__PURE__ */ new Map();
|
|
843
|
-
_InjectableDeps.forEach((deps, node) => {
|
|
844
|
-
inDegree.set(node, inDegree.get(node) || 0);
|
|
845
|
-
deps.forEach((dep) => {
|
|
846
|
-
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.`);
|
|
847
|
-
inDegree.set(dep, inDegree.get(dep) || 0);
|
|
848
|
-
inDegree.set(node, inDegree.get(node) + 1);
|
|
849
|
-
if (!graph.has(dep)) graph.set(dep, []);
|
|
850
|
-
graph.get(dep).push(node);
|
|
851
|
-
});
|
|
852
|
-
});
|
|
853
|
-
const queue = [];
|
|
854
|
-
inDegree.forEach((deg, node) => {
|
|
855
|
-
if (deg === 0) queue.push(node);
|
|
856
|
-
});
|
|
857
|
-
while (queue.length) {
|
|
858
|
-
const current = queue.shift();
|
|
859
|
-
if (!_InjectableRegistry.get(current)) {
|
|
860
|
-
const instance = new current(...(_InjectableDeps.get(current) || []).map((dep) => _InjectableRegistry.get(dep)));
|
|
861
|
-
_InjectableRegistry.set(current, instance);
|
|
862
|
-
}
|
|
863
|
-
(graph.get(current) || []).forEach((neighbor) => {
|
|
864
|
-
inDegree.set(neighbor, (inDegree.get(neighbor) ?? 0) - 1);
|
|
865
|
-
if (inDegree.get(neighbor) === 0) queue.push(neighbor);
|
|
866
|
-
});
|
|
867
|
-
}
|
|
868
|
-
if (!_InjectableRegistry.get(ctor)) throw new Error("Circular dependency detected or injectable not registered");
|
|
869
|
-
return _InjectableRegistry.get(ctor);
|
|
870
|
-
}
|
|
871
|
-
//#endregion
|
|
872
965
|
//#region src/annotation/controller.ts
|
|
873
966
|
const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
|
|
874
967
|
/**
|
|
@@ -987,14 +1080,6 @@ function MiddlewareClass(...args) {
|
|
|
987
1080
|
return Controller(...args);
|
|
988
1081
|
}
|
|
989
1082
|
//#endregion
|
|
990
|
-
//#region \0@oxc-project+runtime@0.127.0/helpers/decorate.js
|
|
991
|
-
function __decorate(decorators, target, key, desc) {
|
|
992
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
993
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
994
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
995
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
996
|
-
}
|
|
997
|
-
//#endregion
|
|
998
1083
|
//#region src/middleware/default/error/base.ts
|
|
999
1084
|
let DefaultBaseErrorMiddleware = class DefaultBaseErrorMiddleware {
|
|
1000
1085
|
handle(err, _request, _response, _next) {
|
|
@@ -1110,4 +1195,22 @@ const DefaultSwaggerMiddleware = {
|
|
|
1110
1195
|
Setup
|
|
1111
1196
|
};
|
|
1112
1197
|
//#endregion
|
|
1113
|
-
|
|
1198
|
+
//#region src/middleware/default/health/index.ts
|
|
1199
|
+
let DefaultHealthMiddleware = class DefaultHealthMiddleware {
|
|
1200
|
+
constructor(healthRegistrar) {
|
|
1201
|
+
this.healthRegistrar = healthRegistrar;
|
|
1202
|
+
}
|
|
1203
|
+
async readiness(_request, _response, _next) {
|
|
1204
|
+
const up = await this.healthRegistrar._readiness();
|
|
1205
|
+
return ResponseEntity.status(up ? 200 : 503).body({ up });
|
|
1206
|
+
}
|
|
1207
|
+
async liveness(_request, _response, _next) {
|
|
1208
|
+
const up = await this.healthRegistrar._liveness();
|
|
1209
|
+
return ResponseEntity.status(up ? 200 : 503).body({ up });
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
__decorate([GET(_settings.health.ready.path)], DefaultHealthMiddleware.prototype, "readiness", null);
|
|
1213
|
+
__decorate([GET(_settings.health.live.path)], DefaultHealthMiddleware.prototype, "liveness", null);
|
|
1214
|
+
DefaultHealthMiddleware = __decorate([MiddlewareClass({ deps: [HealthRegistrar] })], DefaultHealthMiddleware);
|
|
1215
|
+
//#endregion
|
|
1216
|
+
export { Controller, ControllerSchema, DELETE, DefaultBaseErrorMiddleware, DefaultHealthMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, GET, HEAD, HealthRegistrar, Html404ErrorPage, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseBody, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, RouteSchema, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _clearOpenApiRegistry, _getControllerSchema, _getOrCreateSchemaDefinition, _getRouteSchema, _getRoutes, _getValidatorSchema, _parseOrThrow, _registerController, _resolve, _saveValidatorSchema, _setControllerSchema, _setRouteSchema, _settings, generateOpenApiSpec, methodResolve, openApiGenerator };
|