stratal 0.0.18 → 0.0.20
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 +8 -8
- package/dist/{base-email.provider-Cuw4OAB0.mjs → base-email.provider-CfQCA08m.mjs} +1 -1
- package/dist/{base-email.provider-Cuw4OAB0.mjs.map → base-email.provider-CfQCA08m.mjs.map} +1 -1
- package/dist/bin/cloudflare-workers-loader.mjs.map +1 -1
- package/dist/bin/quarry.mjs +26 -35
- package/dist/bin/quarry.mjs.map +1 -1
- package/dist/cache/index.d.mts +2 -153
- package/dist/cache/index.d.mts.map +1 -1
- package/dist/cache/index.mjs +4 -6
- package/dist/cache/index.mjs.map +1 -1
- package/dist/cache.service-DsnKuNyO.d.mts +156 -0
- package/dist/cache.service-DsnKuNyO.d.mts.map +1 -0
- package/dist/cache.tokens-B7Rw1C9Q.mjs +6 -0
- package/dist/cache.tokens-B7Rw1C9Q.mjs.map +1 -0
- package/dist/{colors-BTAnQRGU.mjs → colors-DJaRDXoS.mjs} +1 -1
- package/dist/{colors-BTAnQRGU.mjs.map → colors-DJaRDXoS.mjs.map} +1 -1
- package/dist/{command-DjGqCYHv.mjs → command-BgSlsS4M.mjs} +2 -2
- package/dist/{command-DjGqCYHv.mjs.map → command-BgSlsS4M.mjs.map} +1 -1
- package/dist/{command-B1YuV-UZ.d.mts → command-Bu-PjJrX.d.mts} +2 -2
- package/dist/{command-B1YuV-UZ.d.mts.map → command-Bu-PjJrX.d.mts.map} +1 -1
- package/dist/config/index.d.mts +81 -37
- package/dist/config/index.d.mts.map +1 -1
- package/dist/config/index.mjs +126 -45
- package/dist/config/index.mjs.map +1 -1
- package/dist/{consumer-registry-BkuHXR_u.d.mts → consumer-registry-B7yUNh0q.d.mts} +1 -1
- package/dist/{consumer-registry-BkuHXR_u.d.mts.map → consumer-registry-B7yUNh0q.d.mts.map} +1 -1
- package/dist/controller.decorator-DQzenvSN.mjs +66 -0
- package/dist/controller.decorator-DQzenvSN.mjs.map +1 -0
- package/dist/cron/index.d.mts +4 -3
- package/dist/cron/index.d.mts.map +1 -1
- package/dist/cron/index.mjs +1 -1
- package/dist/{cron-manager-1KnZvojs.mjs → cron-manager-7Symz_TE.mjs} +29 -19
- package/dist/cron-manager-7Symz_TE.mjs.map +1 -0
- package/dist/{cron-manager-BnEZquBL.d.mts → cron-manager-BEsH1mjW.d.mts} +27 -13
- package/dist/cron-manager-BEsH1mjW.d.mts.map +1 -0
- package/dist/di/index.d.mts +1 -1
- package/dist/di/index.mjs +2 -2
- package/dist/email/index.d.mts +3 -3
- package/dist/email/index.mjs +87 -10
- package/dist/email/index.mjs.map +1 -1
- package/dist/{en-3QnZwP-u.mjs → en-DSH_bhh6.mjs} +10 -30
- package/dist/en-DSH_bhh6.mjs.map +1 -0
- package/dist/env-D1rcZ8_r.d.mts +25 -0
- package/dist/env-D1rcZ8_r.d.mts.map +1 -0
- package/dist/errors/index.d.mts +1 -1
- package/dist/errors/index.mjs +1 -1
- package/dist/{errors--RBIvDXr.mjs → errors-BdyV5PnY.mjs} +180 -15
- package/dist/errors-BdyV5PnY.mjs.map +1 -0
- package/dist/{errors-B7hCnXgB.mjs → errors-Da3Pz2X7.mjs} +14 -7
- package/dist/errors-Da3Pz2X7.mjs.map +1 -0
- package/dist/events/index.d.mts +2 -2
- package/dist/events/index.mjs +1 -1
- package/dist/{events-UTJliZhl.mjs → events-COKixqnG.mjs} +2 -2
- package/dist/{events-UTJliZhl.mjs.map → events-COKixqnG.mjs.map} +1 -1
- package/dist/{gateway-context-BdBFoQd8.mjs → gateway-context-CdJjpUCW.mjs} +5 -70
- package/dist/gateway-context-CdJjpUCW.mjs.map +1 -0
- package/dist/guards/index.d.mts +14 -5
- package/dist/guards/index.d.mts.map +1 -1
- package/dist/guards/index.mjs +1 -1
- package/dist/{guards-MtDgcHnF.mjs → guards-DUk_Kzst.mjs} +1 -1
- package/dist/guards-DUk_Kzst.mjs.map +1 -0
- package/dist/http-method.decorator-DXwxAfb_.mjs +96 -0
- package/dist/http-method.decorator-DXwxAfb_.mjs.map +1 -0
- package/dist/i18n/index.d.mts +3 -3
- package/dist/i18n/index.mjs +2 -2
- package/dist/i18n/messages/en/index.d.mts +1 -1
- package/dist/i18n/messages/en/index.mjs +1 -1
- package/dist/i18n/utils/index.mjs +1 -1
- package/dist/i18n/validation/index.d.mts +2 -2
- package/dist/i18n/validation/index.mjs +2 -2
- package/dist/{i18n.module-BpLLLCTg.mjs → i18n.module-BBlNNlcG.mjs} +234 -204
- package/dist/i18n.module-BBlNNlcG.mjs.map +1 -0
- package/dist/index-7-hU3GTV.d.mts +101 -0
- package/dist/index-7-hU3GTV.d.mts.map +1 -0
- package/dist/{index-Dfpd_ypO.d.mts → index-Bnpfq6uk.d.mts} +81 -19
- package/dist/index-Bnpfq6uk.d.mts.map +1 -0
- package/dist/{index-BDh9J2KD.d.mts → index-C1KvMncZ.d.mts} +9 -29
- package/dist/{index-BDh9J2KD.d.mts.map → index-C1KvMncZ.d.mts.map} +1 -1
- package/dist/{index-DPxmo6AY.d.mts → index-CjaQ6_tZ.d.mts} +5 -4
- package/dist/index-CjaQ6_tZ.d.mts.map +1 -0
- package/dist/{index-BrmS34sa.d.mts → index-D0US0X14.d.mts} +375 -235
- package/dist/index-D0US0X14.d.mts.map +1 -0
- package/dist/{index-BR23zDMy.d.mts → index-DBd_2wv8.d.mts} +1 -1
- package/dist/{index-BR23zDMy.d.mts.map → index-DBd_2wv8.d.mts.map} +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{is-command-PvULqiTa.mjs → is-command-C6a7WTPw.mjs} +2 -2
- package/dist/{is-command-PvULqiTa.mjs.map → is-command-C6a7WTPw.mjs.map} +1 -1
- package/dist/{is-seeder-BN9Ej1r7.mjs → is-seeder-CebjZCDn.mjs} +1 -1
- package/dist/{is-seeder-BN9Ej1r7.mjs.map → is-seeder-CebjZCDn.mjs.map} +1 -1
- package/dist/logger/index.d.mts +1 -1
- package/dist/logger/index.mjs +1 -1
- package/dist/{logger-c0ftIK4G.mjs → logger-V6Ms3QnQ.mjs} +38 -20
- package/dist/{logger-c0ftIK4G.mjs.map → logger-V6Ms3QnQ.mjs.map} +1 -1
- package/dist/macroable/index.d.mts +2 -0
- package/dist/macroable/index.mjs +2 -0
- package/dist/macroable-BmufBshB.mjs +122 -0
- package/dist/macroable-BmufBshB.mjs.map +1 -0
- package/dist/module/index.d.mts +2 -2
- package/dist/module/index.mjs +1 -1
- package/dist/{module-C3YZ-kZN.mjs → module-Dk2qTa77.mjs} +160 -19
- package/dist/module-Dk2qTa77.mjs.map +1 -0
- package/dist/openapi/index.d.mts +3 -3
- package/dist/openapi/index.mjs +2 -2
- package/dist/{openapi-tools.service-B77QXD56.mjs → openapi-tools.service-Zs-Ewv7F.mjs} +4 -1
- package/dist/{openapi-tools.service-B77QXD56.mjs.map → openapi-tools.service-Zs-Ewv7F.mjs.map} +1 -1
- package/dist/{openapi.service-6yj0BUY4.d.mts → openapi.service-BLgvn3hJ.d.mts} +3 -3
- package/dist/{openapi.service-6yj0BUY4.d.mts.map → openapi.service-BLgvn3hJ.d.mts.map} +1 -1
- package/dist/quarry/index.d.mts +7 -7
- package/dist/quarry/index.d.mts.map +1 -1
- package/dist/quarry/index.mjs +4 -4
- package/dist/{quarry-registry-CQCIlYTO.mjs → quarry-registry-DNEej-Db.mjs} +17 -15
- package/dist/quarry-registry-DNEej-Db.mjs.map +1 -0
- package/dist/queue/index.d.mts +2 -2
- package/dist/queue/index.mjs +2 -2
- package/dist/{queue.module-DIjD6nr-.mjs → queue.module-BCdCiySt.mjs} +4 -4
- package/dist/{queue.module-DIjD6nr-.mjs.map → queue.module-BCdCiySt.mjs.map} +1 -1
- package/dist/r2-storage.provider-Co6F0ZYV.mjs +244 -0
- package/dist/r2-storage.provider-Co6F0ZYV.mjs.map +1 -0
- package/dist/rate-limit.decorator--o6Q6p9w.mjs +55 -0
- package/dist/rate-limit.decorator--o6Q6p9w.mjs.map +1 -0
- package/dist/rate-limiter/index.d.mts +420 -0
- package/dist/rate-limiter/index.d.mts.map +1 -0
- package/dist/rate-limiter/index.mjs +365 -0
- package/dist/rate-limiter/index.mjs.map +1 -0
- package/dist/{resend.provider-Bvw36rQy.mjs → resend.provider-M6qRLrcy.mjs} +2 -2
- package/dist/{resend.provider-Bvw36rQy.mjs.map → resend.provider-M6qRLrcy.mjs.map} +1 -1
- package/dist/router/index.d.mts +2 -2
- package/dist/router/index.mjs +7 -5
- package/dist/seeder/index.d.mts +3 -3
- package/dist/seeder/index.mjs +2 -2
- package/dist/{seeder-D7VXULXB.mjs → seeder-CJAOHEIo.mjs} +5 -5
- package/dist/{seeder-D7VXULXB.mjs.map → seeder-CJAOHEIo.mjs.map} +1 -1
- package/dist/{setup-BRIN-iYT.mjs → setup-CefZKV_e.mjs} +1 -1
- package/dist/{setup-BRIN-iYT.mjs.map → setup-CefZKV_e.mjs.map} +1 -1
- package/dist/signed-url-BQPbv2In.mjs +74 -0
- package/dist/signed-url-BQPbv2In.mjs.map +1 -0
- package/dist/{smtp.provider-CAwpvzvD.mjs → smtp.provider-w0Ve52Xg.mjs} +2 -2
- package/dist/{smtp.provider-CAwpvzvD.mjs.map → smtp.provider-w0Ve52Xg.mjs.map} +1 -1
- package/dist/storage/index.d.mts +39 -17
- package/dist/storage/index.d.mts.map +1 -1
- package/dist/storage/index.mjs +3 -3
- package/dist/storage/providers/index.d.mts +30 -70
- package/dist/storage/providers/index.d.mts.map +1 -1
- package/dist/storage/providers/index.mjs +2 -2
- package/dist/{storage-CJ-QOwNv.mjs → storage-1zw-6Yiz.mjs} +101 -27
- package/dist/storage-1zw-6Yiz.mjs.map +1 -0
- package/dist/{storage-provider.interface-YRtyYBxV.d.mts → storage-provider.interface-Bd6vA4ak.d.mts} +20 -21
- package/dist/storage-provider.interface-Bd6vA4ak.d.mts.map +1 -0
- package/dist/{stratal-B7G4i9-N.mjs → stratal-DeEcGgdq.mjs} +57 -26
- package/dist/stratal-DeEcGgdq.mjs.map +1 -0
- package/dist/{types-CN0zONAZ.d.mts → types-cySNS_lp.d.mts} +1 -1
- package/dist/types-cySNS_lp.d.mts.map +1 -0
- package/dist/{usage-generator-Cl1HPlUp.mjs → usage-generator-BUdlhnCK.mjs} +2 -2
- package/dist/{usage-generator-Cl1HPlUp.mjs.map → usage-generator-BUdlhnCK.mjs.map} +1 -1
- package/dist/{validation-B4bePOa_.mjs → validation-DtJwAv7O.mjs} +62 -8
- package/dist/validation-DtJwAv7O.mjs.map +1 -0
- package/dist/websocket/index.d.mts +9 -4
- package/dist/websocket/index.d.mts.map +1 -1
- package/dist/websocket/index.mjs +1 -1
- package/dist/workers/index.d.mts +2 -1
- package/dist/workers/index.d.mts.map +1 -1
- package/dist/workers/index.mjs +2 -2
- package/package.json +32 -40
- package/dist/cron-manager-1KnZvojs.mjs.map +0 -1
- package/dist/cron-manager-BnEZquBL.d.mts.map +0 -1
- package/dist/en-3QnZwP-u.mjs.map +0 -1
- package/dist/errors--RBIvDXr.mjs.map +0 -1
- package/dist/errors-B7hCnXgB.mjs.map +0 -1
- package/dist/gateway-context-BdBFoQd8.mjs.map +0 -1
- package/dist/guards-MtDgcHnF.mjs.map +0 -1
- package/dist/i18n.module-BpLLLCTg.mjs.map +0 -1
- package/dist/index-BrmS34sa.d.mts.map +0 -1
- package/dist/index-DPxmo6AY.d.mts.map +0 -1
- package/dist/index-Dfpd_ypO.d.mts.map +0 -1
- package/dist/module-C3YZ-kZN.mjs.map +0 -1
- package/dist/quarry-registry-CQCIlYTO.mjs.map +0 -1
- package/dist/s3-storage.provider-BAhHDMI3.mjs +0 -343
- package/dist/s3-storage.provider-BAhHDMI3.mjs.map +0 -1
- package/dist/storage-CJ-QOwNv.mjs.map +0 -1
- package/dist/storage-provider.interface-YRtyYBxV.d.mts.map +0 -1
- package/dist/stratal-B7G4i9-N.mjs.map +0 -1
- package/dist/types-CN0zONAZ.d.mts.map +0 -1
- package/dist/validation-B4bePOa_.mjs.map +0 -1
package/dist/config/index.mjs
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { A as Scope, H as ApplicationError, k as ERROR_CODES } from "../errors
|
|
2
|
-
import { a as __decorate, f as DI_TOKENS, p as Transient } from "../logger-
|
|
3
|
-
import {
|
|
1
|
+
import { A as Scope, H as ApplicationError, k as ERROR_CODES } from "../errors-BdyV5PnY.mjs";
|
|
2
|
+
import { a as __decorate, f as DI_TOKENS, o as __decorateParam, p as Transient, s as __decorateMetadata } from "../logger-V6Ms3QnQ.mjs";
|
|
3
|
+
import { t as Macroable } from "../macroable-BmufBshB.mjs";
|
|
4
|
+
import { k as Module } from "../module-Dk2qTa77.mjs";
|
|
5
|
+
import { inject } from "tsyringe";
|
|
4
6
|
//#region src/config/config.tokens.ts
|
|
5
|
-
const CONFIG_TOKENS = {
|
|
7
|
+
const CONFIG_TOKENS = {
|
|
8
|
+
ConfigService: Symbol.for("stratal:config:service"),
|
|
9
|
+
ConfigStore: Symbol.for("stratal:config:store")
|
|
10
|
+
};
|
|
6
11
|
//#endregion
|
|
7
12
|
//#region src/config/config.types.ts
|
|
8
13
|
var ConfigValidationError = class extends Error {
|
|
@@ -40,69 +45,77 @@ var ConfigNotInitializedError = class extends ApplicationError {
|
|
|
40
45
|
};
|
|
41
46
|
//#endregion
|
|
42
47
|
//#region src/config/services/config.service.ts
|
|
43
|
-
let ConfigService = class ConfigService {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* Called by ConfigModule during initialization
|
|
49
|
-
*/
|
|
50
|
-
initialize(config) {
|
|
51
|
-
this.originalConfig = this.deepClone(config);
|
|
52
|
-
this.currentConfig = this.deepClone(config);
|
|
48
|
+
let ConfigService = class ConfigService extends Macroable {
|
|
49
|
+
overrides = /* @__PURE__ */ new Map();
|
|
50
|
+
constructor(store) {
|
|
51
|
+
super();
|
|
52
|
+
this.store = store;
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
* Get config value using dot notation
|
|
56
|
-
*
|
|
55
|
+
* Get config value using dot notation. Request overrides take
|
|
56
|
+
* precedence over the shared store.
|
|
57
57
|
*/
|
|
58
58
|
get(path) {
|
|
59
|
-
this.
|
|
60
|
-
|
|
59
|
+
const override = this.readOverride(path);
|
|
60
|
+
if (override !== void 0) return override;
|
|
61
|
+
return this.store.get(path);
|
|
61
62
|
}
|
|
62
63
|
/**
|
|
63
|
-
* Set config value
|
|
64
|
-
*
|
|
64
|
+
* Set a config value for the lifetime of the current request.
|
|
65
|
+
* Does not mutate the shared store.
|
|
65
66
|
*/
|
|
66
67
|
set(path, value) {
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
68
|
+
if (this.hasDangerousSegment(path)) return;
|
|
69
|
+
this.overrides.set(path, value);
|
|
69
70
|
}
|
|
70
71
|
/**
|
|
71
|
-
*
|
|
72
|
-
* @param path - Optional path to reset (resets entire config if omitted)
|
|
72
|
+
* Clear a single override, or all overrides for this request.
|
|
73
73
|
*/
|
|
74
74
|
reset(path) {
|
|
75
|
-
this.ensureInitialized();
|
|
76
75
|
if (path) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
76
|
+
this.overrides.delete(path);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.overrides.clear();
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* Get
|
|
82
|
+
* Get the full config object, with request overrides merged in.
|
|
83
83
|
*/
|
|
84
84
|
all() {
|
|
85
|
-
this.
|
|
86
|
-
|
|
85
|
+
const base = this.store.all();
|
|
86
|
+
if (this.overrides.size === 0) return base;
|
|
87
|
+
const merged = this.deepClone(base);
|
|
88
|
+
for (const [path, value] of this.overrides) this.writeByPath(merged, path, value);
|
|
89
|
+
return merged;
|
|
87
90
|
}
|
|
88
91
|
/**
|
|
89
|
-
* Check if a config path exists
|
|
92
|
+
* Check if a config path exists (in overrides or the store).
|
|
90
93
|
*/
|
|
91
94
|
has(path) {
|
|
92
|
-
this.
|
|
93
|
-
return this.
|
|
95
|
+
if (this.readOverride(path) !== void 0) return true;
|
|
96
|
+
return this.store.has(path);
|
|
94
97
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
readOverride(path) {
|
|
99
|
+
if (this.hasDangerousSegment(path)) return void 0;
|
|
100
|
+
if (this.overrides.has(path)) return this.overrides.get(path);
|
|
101
|
+
const segments = path.split(".");
|
|
102
|
+
for (let i = segments.length - 1; i > 0; i--) {
|
|
103
|
+
const parent = segments.slice(0, i).join(".");
|
|
104
|
+
if (this.overrides.has(parent)) {
|
|
105
|
+
const parentValue = this.overrides.get(parent);
|
|
106
|
+
return this.walk(parentValue, segments.slice(i));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
walk(value, keys) {
|
|
111
|
+
let current = value;
|
|
98
112
|
for (const key of keys) {
|
|
99
|
-
if (this.isDangerousKey(key)) return void 0;
|
|
100
113
|
if (current === null || current === void 0) return void 0;
|
|
101
114
|
current = current[key];
|
|
102
115
|
}
|
|
103
116
|
return current;
|
|
104
117
|
}
|
|
105
|
-
|
|
118
|
+
writeByPath(obj, path, value) {
|
|
106
119
|
const keys = path.split(".");
|
|
107
120
|
if (keys.some((key) => this.isDangerousKey(key))) return;
|
|
108
121
|
let current = obj;
|
|
@@ -123,18 +136,82 @@ let ConfigService = class ConfigService {
|
|
|
123
136
|
configurable: true
|
|
124
137
|
});
|
|
125
138
|
}
|
|
139
|
+
hasDangerousSegment(path) {
|
|
140
|
+
return path.split(".").some((key) => this.isDangerousKey(key));
|
|
141
|
+
}
|
|
142
|
+
isDangerousKey(key) {
|
|
143
|
+
return key === "__proto__" || key === "constructor" || key === "prototype";
|
|
144
|
+
}
|
|
145
|
+
deepClone(obj) {
|
|
146
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
147
|
+
return JSON.parse(JSON.stringify(obj));
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
ConfigService = __decorate([
|
|
151
|
+
Transient(CONFIG_TOKENS.ConfigService),
|
|
152
|
+
__decorateParam(0, inject(CONFIG_TOKENS.ConfigStore)),
|
|
153
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
154
|
+
], ConfigService);
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/config/services/config.store.ts
|
|
157
|
+
let ConfigStore = class ConfigStore {
|
|
158
|
+
data;
|
|
159
|
+
/**
|
|
160
|
+
* Initialize the store with validated configuration.
|
|
161
|
+
* Called by {@link ConfigModule} during initialization.
|
|
162
|
+
*/
|
|
163
|
+
initialize(config) {
|
|
164
|
+
this.data = this.deepClone(config);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get config value using dot notation.
|
|
168
|
+
*/
|
|
169
|
+
get(path) {
|
|
170
|
+
this.ensureInitialized();
|
|
171
|
+
return this.getByPath(this.data, path);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Check if a config path exists.
|
|
175
|
+
*/
|
|
176
|
+
has(path) {
|
|
177
|
+
this.ensureInitialized();
|
|
178
|
+
return this.getByPath(this.data, path) !== void 0;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get the entire config object (readonly snapshot).
|
|
182
|
+
*/
|
|
183
|
+
all() {
|
|
184
|
+
this.ensureInitialized();
|
|
185
|
+
return this.data;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* True once {@link initialize} has been called.
|
|
189
|
+
*/
|
|
190
|
+
isInitialized() {
|
|
191
|
+
return this.data !== void 0;
|
|
192
|
+
}
|
|
193
|
+
getByPath(obj, path) {
|
|
194
|
+
const keys = path.split(".");
|
|
195
|
+
let current = obj;
|
|
196
|
+
for (const key of keys) {
|
|
197
|
+
if (this.isDangerousKey(key)) return void 0;
|
|
198
|
+
if (current === null || current === void 0) return void 0;
|
|
199
|
+
current = current[key];
|
|
200
|
+
}
|
|
201
|
+
return current;
|
|
202
|
+
}
|
|
126
203
|
isDangerousKey(key) {
|
|
127
204
|
return key === "__proto__" || key === "constructor" || key === "prototype";
|
|
128
205
|
}
|
|
129
206
|
ensureInitialized() {
|
|
130
|
-
if (
|
|
207
|
+
if (this.data === void 0) throw new ConfigNotInitializedError();
|
|
131
208
|
}
|
|
132
209
|
deepClone(obj) {
|
|
133
210
|
if (obj === null || typeof obj !== "object") return obj;
|
|
134
211
|
return JSON.parse(JSON.stringify(obj));
|
|
135
212
|
}
|
|
136
213
|
};
|
|
137
|
-
|
|
214
|
+
ConfigStore = __decorate([Transient(CONFIG_TOKENS.ConfigStore)], ConfigStore);
|
|
138
215
|
//#endregion
|
|
139
216
|
//#region src/config/config.module.ts
|
|
140
217
|
var _ConfigModule;
|
|
@@ -162,21 +239,25 @@ let ConfigModule = _ConfigModule = class ConfigModule {
|
|
|
162
239
|
onInitialize(context) {
|
|
163
240
|
if (!moduleOptions) throw new ConfigModuleNotInitializedError();
|
|
164
241
|
const env = context.container.resolve(DI_TOKENS.CloudflareEnv);
|
|
165
|
-
const
|
|
242
|
+
const configStore = context.container.resolve(CONFIG_TOKENS.ConfigStore);
|
|
166
243
|
const mergedConfig = {};
|
|
167
244
|
for (const namespace of moduleOptions.load) mergedConfig[namespace.namespace] = namespace.factory(env);
|
|
168
245
|
if (moduleOptions.validateSchema) {
|
|
169
246
|
const result = moduleOptions.validateSchema.safeParse(mergedConfig);
|
|
170
247
|
if (!result.success) throw new ConfigValidationError("Configuration validation failed", result.error);
|
|
171
248
|
}
|
|
172
|
-
|
|
249
|
+
configStore.initialize(mergedConfig);
|
|
173
250
|
context.logger.debug("ConfigModule initialized", { namespaces: moduleOptions.load.map((n) => n.namespace) });
|
|
174
251
|
}
|
|
175
252
|
};
|
|
176
253
|
ConfigModule = _ConfigModule = __decorate([Module({ providers: [{
|
|
254
|
+
provide: CONFIG_TOKENS.ConfigStore,
|
|
255
|
+
useClass: ConfigStore,
|
|
256
|
+
scope: Scope.Singleton
|
|
257
|
+
}, {
|
|
177
258
|
provide: CONFIG_TOKENS.ConfigService,
|
|
178
259
|
useClass: ConfigService,
|
|
179
|
-
scope: Scope.
|
|
260
|
+
scope: Scope.Request
|
|
180
261
|
}] })], ConfigModule);
|
|
181
262
|
//#endregion
|
|
182
263
|
//#region src/config/register-as.ts
|
|
@@ -227,6 +308,6 @@ function registerAs(namespace, factory) {
|
|
|
227
308
|
};
|
|
228
309
|
}
|
|
229
310
|
//#endregion
|
|
230
|
-
export { CONFIG_TOKENS, ConfigModule, ConfigService, ConfigValidationError, registerAs };
|
|
311
|
+
export { CONFIG_TOKENS, ConfigModule, ConfigService, ConfigStore, ConfigValidationError, registerAs };
|
|
231
312
|
|
|
232
313
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/config/config.tokens.ts","../../src/config/config.types.ts","../../src/config/errors/config-module-not-initialized.error.ts","../../src/config/errors/config-not-initialized.error.ts","../../src/config/services/config.service.ts","../../src/config/config.module.ts","../../src/config/register-as.ts"],"sourcesContent":["export const CONFIG_TOKENS = {\n\tConfigService: Symbol.for('stratal:config:service'),\n} as const\n","import type { z } from '../i18n/validation'\n\nexport class ConfigValidationError extends Error {\n constructor(\n message: string,\n public readonly errors: z.ZodError\n ) {\n super(message)\n this.name = 'ConfigValidationError'\n }\n}\n\n/**\n * Configuration that can be augmented by applications\n * Apps should augment this interface with their AppConfig type using module augmentation\n *\n * @example\n * ```typescript\n * // In your app (e.g., apps/backend/src/config/types.ts)\n * declare module 'stratal' {\n * interface ModuleConfig {\n * database: { url: string; maxConnections: number }\n * email: { provider: string; from: { name: string; email: string } }\n * }\n * }\n * ```\n */\nexport interface ModuleConfig { }\n\n/**\n * Generate all valid dot-notation paths from a config object type\n * @example ConfigPath<{ database: { url: string } }> = 'database' | 'database.url'\n */\nexport type ConfigPath<T> = {\n [K in keyof T & string]: T[K] extends Record<string, unknown>\n ? K | `${K}.${ConfigPath<T[K]>}`\n : K\n}[keyof T & string]\n\n/**\n * Get the value type at a dot-notation path\n * @example ConfigPathValue<{ database: { url: string } }, 'database.url'> = string\n */\nexport type ConfigPathValue<T, P extends string> = P extends `${infer K}.${infer Rest}`\n ? K extends keyof T\n ? T[K] extends Record<string, unknown>\n ? ConfigPathValue<T[K], Rest>\n : never\n : never\n : P extends keyof T\n ? T[P]\n : never\n\n/**\n * ConfigService interface with dot notation support\n */\nexport interface IConfigService<T extends object = ModuleConfig> {\n /**\n * Initialize the config service with validated configuration\n * Should be called once during application startup\n */\n initialize(config: T): void\n\n /**\n * Get config value using dot notation\n * @example config.get('database.url')\n */\n get<P extends ConfigPath<T>>(path: P): ConfigPathValue<T, P>\n\n /**\n * Set config value at runtime (for runtime overrides)\n * @example config.set('email.from.name', 'Custom Name')\n */\n set<P extends ConfigPath<T>>(path: P, value: ConfigPathValue<T, P>): void\n\n /**\n * Reset config to original value\n * @param path - Optional path to reset (resets entire config if omitted)\n */\n reset(path?: ConfigPath<T>): void\n\n /**\n * Get entire config object\n */\n all(): Readonly<T>\n\n /**\n * Check if a config path exists\n */\n has(path: ConfigPath<T>): boolean\n}\n","import { ApplicationError, ERROR_CODES } from '../../errors'\n\n/**\n * Error thrown when ConfigModule's onInitialize runs but forRoot() was never called.\n *\n * This means the module was imported without calling ConfigModule.forRoot({ load: [...] }).\n */\nexport class ConfigModuleNotInitializedError extends ApplicationError {\n constructor() {\n super(\n 'errors.configModuleNotInitialized',\n ERROR_CODES.SYSTEM.CONFIG_MODULE_NOT_INITIALIZED\n )\n }\n}\n","import { ERROR_CODES } from '../../errors'\nimport { ApplicationError } from '../../errors'\n\n/**\n * ConfigNotInitializedError\n *\n * Thrown when attempting to access ConfigService before it has been initialized.\n * This typically indicates that ConfigModule's onInitialize hook hasn't run yet,\n * or the module wasn't registered properly.\n */\nexport class ConfigNotInitializedError extends ApplicationError {\n constructor() {\n super(\n 'errors.configNotInitialized',\n ERROR_CODES.SYSTEM.CONFIG_NOT_INITIALIZED\n )\n }\n}\n","import { Transient } from '../../di/decorators'\nimport { CONFIG_TOKENS } from '../config.tokens'\nimport type { ConfigPath, ConfigPathValue, IConfigService, ModuleConfig } from '../config.types'\nimport { ConfigNotInitializedError } from '../errors'\n\n/**\n * ConfigService with dot notation support for get/set operations\n *\n * Supports runtime overrides via set() - useful for request-specific config overrides.\n * Use reset() to restore original values.\n *\n * @example\n * ```typescript\n * // Get with dot notation\n * const url = config.get('database.url')\n * const fromName = config.get('email.from.name')\n *\n * // Set at runtime (e.g., in middleware for runtime override)\n * config.set('email.from.name', 'Custom Name')\n *\n * // Reset to original\n * config.reset('email.from.name') // reset specific path\n * config.reset() // reset entire config\n * ```\n */\n@Transient(CONFIG_TOKENS.ConfigService)\nexport class ConfigService<T extends object = ModuleConfig> implements IConfigService<T> {\n private originalConfig: T | undefined\n private currentConfig: T | undefined\n\n /**\n * Initialize the config service with validated configuration\n * Called by ConfigModule during initialization\n */\n initialize(config: T): void {\n this.originalConfig = this.deepClone(config)\n this.currentConfig = this.deepClone(config)\n }\n\n /**\n * Get config value using dot notation\n * @example config.get('database.url')\n */\n get<P extends ConfigPath<T>>(path: P): ConfigPathValue<T, P> {\n this.ensureInitialized()\n return this.getByPath(this.currentConfig, path) as ConfigPathValue<T, P>\n }\n\n /**\n * Set config value at runtime (for runtime overrides)\n * @example config.set('email.from.name', 'Custom Name')\n */\n set<P extends ConfigPath<T>>(path: P, value: ConfigPathValue<T, P>): void {\n this.ensureInitialized()\n this.setByPath(this.currentConfig, path, value)\n }\n\n /**\n * Reset config to original value\n * @param path - Optional path to reset (resets entire config if omitted)\n */\n reset(path?: ConfigPath<T>): void {\n this.ensureInitialized()\n if (path) {\n const originalValue = this.getByPath(this.originalConfig, path)\n this.setByPath(this.currentConfig, path, this.deepClone(originalValue))\n } else {\n this.currentConfig = this.deepClone(this.originalConfig)\n }\n }\n\n /**\n * Get entire config object\n */\n all(): Readonly<T> {\n this.ensureInitialized()\n return this.currentConfig as Readonly<T>\n }\n\n /**\n * Check if a config path exists\n */\n has(path: ConfigPath<T>): boolean {\n this.ensureInitialized()\n return this.getByPath(this.currentConfig, path) !== undefined\n }\n\n private getByPath(obj: unknown, path: string): unknown {\n const keys = path.split('.')\n let current = obj\n for (const key of keys) {\n if (this.isDangerousKey(key)) return undefined\n if (current === null || current === undefined) return undefined\n current = (current as Record<string, unknown>)[key]\n }\n return current\n }\n\n private setByPath(obj: unknown, path: string, value: unknown): void {\n const keys = path.split('.')\n if (keys.some((key) => this.isDangerousKey(key))) return\n let current = obj as Record<string, unknown>\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i]\n if (!Object.hasOwn(current, key) || typeof current[key] !== 'object' || current[key] === null) {\n Object.defineProperty(current, key, {\n value: {},\n writable: true,\n enumerable: true,\n configurable: true,\n })\n }\n current = current[key] as Record<string, unknown>\n }\n Object.defineProperty(current, keys[keys.length - 1], {\n value,\n writable: true,\n enumerable: true,\n configurable: true,\n })\n }\n\n private isDangerousKey(key: string): boolean {\n return key === '__proto__' || key === 'constructor' || key === 'prototype'\n }\n\n private ensureInitialized(): void {\n if (!this.currentConfig) {\n throw new ConfigNotInitializedError()\n }\n }\n\n private deepClone<V>(obj: V): V {\n if (obj === null || typeof obj !== 'object') {\n return obj\n }\n return JSON.parse(JSON.stringify(obj)) as V\n }\n}\n","import { DI_TOKENS } from '../di/tokens'\nimport { Scope } from '../di/types'\nimport type { z } from '../i18n/validation'\nimport { Module } from '../module'\nimport type { DynamicModule, ModuleContext, OnInitialize, Provider } from '../module/types'\nimport { CONFIG_TOKENS } from './config.tokens'\nimport { ConfigValidationError, type ModuleConfig } from './config.types'\nimport { ConfigModuleNotInitializedError } from './errors'\nimport type { ConfigNamespace } from './register-as'\nimport { ConfigService } from './services/config.service'\n\n/**\n * Any config namespace - uses structural typing for flexibility\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyConfigNamespace = ConfigNamespace<string, any, object>\n\n/**\n * Options for ConfigModule.forRoot\n */\nexport interface ConfigModuleOptions {\n /**\n * Array of config namespaces created via registerAs()\n */\n load: AnyConfigNamespace[]\n\n /**\n * Optional Zod schema for validating the merged config\n * Validates the entire config object after all namespaces are merged\n */\n validateSchema?: z.ZodType<object>\n}\n\n// Store options for use in onInitialize\nlet moduleOptions: ConfigModuleOptions | null = null\n\n/**\n * ConfigModule\n *\n * Provides configuration management with namespace support.\n * Uses registerAs() to create typed config namespaces that can be injected.\n *\n * @example\n * ```typescript\n * // Define config namespaces\n * const databaseConfig = registerAs('database', (env) => ({\n * url: env.DATABASE_URL,\n * maxConnections: 10\n * }))\n *\n * const emailConfig = registerAs('email', (env) => ({\n * provider: env.EMAIL_PROVIDER,\n * from: { name: 'App', email: 'noreply@example.com' }\n * }))\n *\n * // Register in module\n * @Module({\n * imports: [\n * ConfigModule.forRoot({\n * load: [databaseConfig, emailConfig],\n * validateSchema: AppConfigSchema\n * })\n * ]\n * })\n * export class AppModule {}\n *\n * // Inject config\n * constructor(\n * @inject(CONFIG_TOKENS.ConfigService) private config: IConfigService,\n * @inject(databaseConfig.KEY) private dbConfig: DatabaseConfig\n * ) {\n * // Use dot notation\n * const url = this.config.get('database.url')\n *\n * // Or inject namespace directly\n * const url = this.dbConfig.url\n * }\n * ```\n */\n@Module({\n providers: [\n // Register the main ConfigService as Singleton so initialization persists\n {\n provide: CONFIG_TOKENS.ConfigService,\n useClass: ConfigService,\n scope: Scope.Singleton,\n },\n ],\n})\nexport class ConfigModule implements OnInitialize {\n /**\n * Configure ConfigModule with namespace loaders\n *\n * @param options - Configuration options\n * @returns Dynamic module with config infrastructure\n */\n static forRoot(options: ConfigModuleOptions): DynamicModule {\n moduleOptions = options\n\n const providers: Provider[] = []\n\n // Register each namespace config using asProvider()\n for (const namespace of options.load) {\n providers.push(namespace.asProvider())\n }\n\n return {\n module: ConfigModule,\n providers,\n }\n }\n\n /**\n * Initialize config service with merged namespaces\n * Called after all providers are registered\n */\n onInitialize(context: ModuleContext): void {\n if (!moduleOptions) {\n throw new ConfigModuleNotInitializedError()\n }\n\n const env = context.container.resolve<unknown>(DI_TOKENS.CloudflareEnv)\n const configService = context.container.resolve<ConfigService>(CONFIG_TOKENS.ConfigService)\n\n // Build merged config from all namespaces\n const mergedConfig: Record<string, unknown> = {}\n\n for (const namespace of moduleOptions.load) {\n mergedConfig[namespace.namespace] = namespace.factory(env)\n }\n\n // Validate if schema provided\n if (moduleOptions.validateSchema) {\n const result = moduleOptions.validateSchema.safeParse(mergedConfig)\n if (!result.success) {\n throw new ConfigValidationError(\n 'Configuration validation failed',\n result.error\n )\n }\n }\n\n // Initialize ConfigService with merged config\n configService.initialize(mergedConfig as ModuleConfig)\n\n context.logger.debug('ConfigModule initialized', {\n namespaces: moduleOptions.load.map((n) => n.namespace),\n })\n }\n}\n","import type { InjectionToken } from 'tsyringe'\nimport { DI_TOKENS } from '../di/tokens'\nimport type { FactoryProvider } from '../module/types'\n\n/**\n * Configuration namespace registration result\n */\nexport interface ConfigNamespace<TKey extends string, TEnv, TConfig extends object> {\n /** Auto-derived injection token (e.g., 'database' -> Symbol('stratal:config:database')) */\n readonly KEY: InjectionToken<TConfig>\n /** The namespace key */\n readonly namespace: TKey\n /** The factory function that receives env and returns config */\n readonly factory: (env: TEnv) => TConfig\n /**\n * Returns a provider configuration for use in module registration\n * Automatically injects DI_TOKENS.CloudflareEnv\n */\n asProvider(): FactoryProvider<TConfig>\n}\n\n/**\n * Create a namespaced configuration factory\n * Similar to NestJS registerAs\n *\n * @param namespace - Configuration namespace (e.g., 'database', 'email')\n * @param factory - Factory function receiving env and returning config object\n * @returns ConfigNamespace with token, factory, and asProvider() method\n *\n * @example\n * ```typescript\n * // apps/backend/src/config/database.config.ts\n * export const databaseConfig = registerAs('database', (env: Env) => ({\n * url: env.DATABASE_URL,\n * maxConnections: parseInt(env.DATABASE_MAX_CONNECTIONS || '10'),\n * }))\n *\n * // Auto-generates: databaseConfig.KEY = Symbol('stratal:config:database')\n *\n * // Usage in module:\n * // Option 1: Manual provider\n * {\n * provide: databaseConfig.KEY,\n * useFactory: databaseConfig.factory,\n * inject: [DI_TOKENS.CloudflareEnv]\n * }\n *\n * // Option 2: asProvider() helper\n * databaseConfig.asProvider()\n * // Returns: { provide: databaseConfig.KEY, useFactory: ..., inject: [DI_TOKENS.CloudflareEnv] }\n * ```\n */\nexport function registerAs<TKey extends string, TEnv, TConfig extends object>(\n namespace: TKey,\n factory: (env: TEnv) => TConfig\n): ConfigNamespace<TKey, TEnv, TConfig> {\n const KEY = Symbol.for(`stratal:config:${namespace}`) as InjectionToken<TConfig>\n\n return {\n KEY,\n namespace,\n factory,\n asProvider(): FactoryProvider<TConfig> {\n return {\n provide: KEY,\n useFactory: factory,\n inject: [DI_TOKENS.CloudflareEnv],\n }\n },\n }\n}\n\n/**\n * Helper to derive config type from registerAs result\n *\n * @example\n * ```typescript\n * const databaseConfig = registerAs('database', (env) => ({ url: env.DATABASE_URL }))\n * type DatabaseConfig = InferConfigType<typeof databaseConfig>\n * // { url: string }\n * ```\n */\nexport type InferConfigType<T> = T extends ConfigNamespace<string, unknown, infer C> ? C : never\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/config/config.tokens.ts","../../src/config/config.types.ts","../../src/config/errors/config-module-not-initialized.error.ts","../../src/config/errors/config-not-initialized.error.ts","../../src/config/services/config.service.ts","../../src/config/services/config.store.ts","../../src/config/config.module.ts","../../src/config/register-as.ts"],"sourcesContent":["export const CONFIG_TOKENS = {\n\tConfigService: Symbol.for('stratal:config:service'),\n\tConfigStore: Symbol.for('stratal:config:store'),\n} as const\n","import type { z } from '../i18n/validation'\n\nexport class ConfigValidationError extends Error {\n constructor(\n message: string,\n public readonly errors: z.ZodError\n ) {\n super(message)\n this.name = 'ConfigValidationError'\n }\n}\n\n/**\n * Configuration that can be augmented by applications\n * Apps should augment this interface with their AppConfig type using module augmentation\n *\n * @example\n * ```typescript\n * // In your app (e.g., apps/backend/src/config/types.ts)\n * declare module 'stratal' {\n * interface ModuleConfig {\n * database: { url: string; maxConnections: number }\n * email: { provider: string; from: { name: string; email: string } }\n * }\n * }\n * ```\n */\nexport interface ModuleConfig { }\n\n/**\n * Generate all valid dot-notation paths from a config object type\n * @example ConfigPath<{ database: { url: string } }> = 'database' | 'database.url'\n */\nexport type ConfigPath<T> = {\n [K in keyof T & string]: T[K] extends Record<string, unknown>\n ? K | `${K}.${ConfigPath<T[K]>}`\n : K\n}[keyof T & string]\n\n/**\n * Get the value type at a dot-notation path\n * @example ConfigPathValue<{ database: { url: string } }, 'database.url'> = string\n */\nexport type ConfigPathValue<T, P extends string> = P extends `${infer K}.${infer Rest}`\n ? K extends keyof T\n ? T[K] extends Record<string, unknown>\n ? ConfigPathValue<T[K], Rest>\n : never\n : never\n : P extends keyof T\n ? T[P]\n : never\n\n/**\n * ConfigService interface with dot notation support.\n *\n * Values are initialized on the underlying {@link ConfigStore} via\n * {@link ConfigModule}; the service itself exposes request-scoped\n * reads and per-request overrides.\n */\nexport interface IConfigService<T extends object = ModuleConfig> {\n /**\n * Get config value using dot notation\n * @example config.get('database.url')\n */\n get<P extends ConfigPath<T>>(path: P): ConfigPathValue<T, P>\n\n /**\n * Set config value at runtime (for runtime overrides)\n * @example config.set('email.from.name', 'Custom Name')\n */\n set<P extends ConfigPath<T>>(path: P, value: ConfigPathValue<T, P>): void\n\n /**\n * Reset config to original value\n * @param path - Optional path to reset (resets entire config if omitted)\n */\n reset(path?: ConfigPath<T>): void\n\n /**\n * Get entire config object\n */\n all(): Readonly<T>\n\n /**\n * Check if a config path exists\n */\n has(path: ConfigPath<T>): boolean\n}\n","import { ApplicationError, ERROR_CODES } from '../../errors'\n\n/**\n * Error thrown when ConfigModule's onInitialize runs but forRoot() was never called.\n *\n * This means the module was imported without calling ConfigModule.forRoot({ load: [...] }).\n */\nexport class ConfigModuleNotInitializedError extends ApplicationError {\n constructor() {\n super(\n 'errors.configModuleNotInitialized',\n ERROR_CODES.SYSTEM.CONFIG_MODULE_NOT_INITIALIZED\n )\n }\n}\n","import { ERROR_CODES } from '../../errors'\nimport { ApplicationError } from '../../errors'\n\n/**\n * ConfigNotInitializedError\n *\n * Thrown when attempting to access ConfigService before it has been initialized.\n * This typically indicates that ConfigModule's onInitialize hook hasn't run yet,\n * or the module wasn't registered properly.\n */\nexport class ConfigNotInitializedError extends ApplicationError {\n constructor() {\n super(\n 'errors.configNotInitialized',\n ERROR_CODES.SYSTEM.CONFIG_NOT_INITIALIZED\n )\n }\n}\n","import { inject } from 'tsyringe'\nimport { Transient } from '../../di/decorators'\nimport { Macroable } from '../../macroable/macroable'\nimport { CONFIG_TOKENS } from '../config.tokens'\nimport type { ConfigPath, ConfigPathValue, IConfigService, ModuleConfig } from '../config.types'\nimport { type ConfigStore } from './config.store'\n\n/**\n * ConfigService with dot notation support and per-request overrides.\n *\n * ConfigService is **request-scoped**: each request gets its own\n * instance with a private `overrides` map layered over the shared\n * {@link ConfigStore}. Calls to {@link set} mutate only the current\n * request's overrides, which makes it safe to mutate config from\n * middleware (e.g. to pin `environment.appUrl` to the request host).\n *\n * Extends {@link Macroable} so apps can add domain-specific getters\n * and methods via `ConfigService.getter()` / `ConfigService.macro()`.\n *\n * @example\n * ```typescript\n * // Read with dot notation\n * const url = config.get('database.url')\n * const fromName = config.get('email.from.name')\n *\n * // Per-request override (e.g. in middleware)\n * config.set('environment.appUrl', `${proto}://${host}`)\n *\n * // Reset the override for the current request\n * config.reset('environment.appUrl')\n * ```\n */\n@Transient(CONFIG_TOKENS.ConfigService)\nexport class ConfigService<T extends object = ModuleConfig> extends Macroable implements IConfigService<T> {\n private overrides = new Map<string, unknown>()\n\n constructor(\n @inject(CONFIG_TOKENS.ConfigStore) private readonly store: ConfigStore<T>,\n ) {\n super()\n }\n\n /**\n * Get config value using dot notation. Request overrides take\n * precedence over the shared store.\n */\n get<P extends ConfigPath<T>>(path: P): ConfigPathValue<T, P> {\n const override = this.readOverride(path)\n if (override !== undefined) {\n return override as ConfigPathValue<T, P>\n }\n return this.store.get(path)\n }\n\n /**\n * Set a config value for the lifetime of the current request.\n * Does not mutate the shared store.\n */\n set<P extends ConfigPath<T>>(path: P, value: ConfigPathValue<T, P>): void {\n if (this.hasDangerousSegment(path)) return\n this.overrides.set(path, value)\n }\n\n /**\n * Clear a single override, or all overrides for this request.\n */\n reset(path?: ConfigPath<T>): void {\n if (path) {\n this.overrides.delete(path)\n return\n }\n this.overrides.clear()\n }\n\n /**\n * Get the full config object, with request overrides merged in.\n */\n all(): Readonly<T> {\n const base = this.store.all() as T\n if (this.overrides.size === 0) {\n return base as Readonly<T>\n }\n const merged = this.deepClone(base)\n for (const [path, value] of this.overrides) {\n this.writeByPath(merged, path, value)\n }\n return merged as Readonly<T>\n }\n\n /**\n * Check if a config path exists (in overrides or the store).\n */\n has(path: ConfigPath<T>): boolean {\n if (this.readOverride(path) !== undefined) return true\n return this.store.has(path)\n }\n\n private readOverride(path: string): unknown {\n if (this.hasDangerousSegment(path)) return undefined\n if (this.overrides.has(path)) {\n return this.overrides.get(path)\n }\n // Support partial-path reads: if an ancestor was overridden, walk into it.\n const segments = path.split('.')\n for (let i = segments.length - 1; i > 0; i--) {\n const parent = segments.slice(0, i).join('.')\n if (this.overrides.has(parent)) {\n const parentValue = this.overrides.get(parent)\n return this.walk(parentValue, segments.slice(i))\n }\n }\n return undefined\n }\n\n private walk(value: unknown, keys: string[]): unknown {\n let current = value\n for (const key of keys) {\n if (current === null || current === undefined) return undefined\n current = (current as Record<string, unknown>)[key]\n }\n return current\n }\n\n private writeByPath(obj: unknown, path: string, value: unknown): void {\n const keys = path.split('.')\n if (keys.some((key) => this.isDangerousKey(key))) return\n let current = obj as Record<string, unknown>\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i]\n if (!Object.hasOwn(current, key) || typeof current[key] !== 'object' || current[key] === null) {\n Object.defineProperty(current, key, {\n value: {},\n writable: true,\n enumerable: true,\n configurable: true,\n })\n }\n current = current[key] as Record<string, unknown>\n }\n Object.defineProperty(current, keys[keys.length - 1], {\n value,\n writable: true,\n enumerable: true,\n configurable: true,\n })\n }\n\n private hasDangerousSegment(path: string): boolean {\n return path.split('.').some((key) => this.isDangerousKey(key))\n }\n\n private isDangerousKey(key: string): boolean {\n return key === '__proto__' || key === 'constructor' || key === 'prototype'\n }\n\n private deepClone<V>(obj: V): V {\n if (obj === null || typeof obj !== 'object') {\n return obj\n }\n return JSON.parse(JSON.stringify(obj)) as V\n }\n}\n","import { Transient } from '../../di/decorators'\nimport { CONFIG_TOKENS } from '../config.tokens'\nimport type { ConfigPath, ConfigPathValue, ModuleConfig } from '../config.types'\nimport { ConfigNotInitializedError } from '../errors'\n\n/**\n * ConfigStore\n *\n * Singleton-scoped holder of validated, merged configuration.\n *\n * ConfigStore is the source of truth for configuration values. It is\n * initialized once during application startup by {@link ConfigModule}\n * and never mutated afterwards.\n *\n * Per-request overrides live on {@link ConfigService}, which reads\n * through to this store for any key not explicitly overridden.\n */\n@Transient(CONFIG_TOKENS.ConfigStore)\nexport class ConfigStore<T extends object = ModuleConfig> {\n private data: T | undefined\n\n /**\n * Initialize the store with validated configuration.\n * Called by {@link ConfigModule} during initialization.\n */\n initialize(config: T): void {\n this.data = this.deepClone(config)\n }\n\n /**\n * Get config value using dot notation.\n */\n get<P extends ConfigPath<T>>(path: P): ConfigPathValue<T, P> {\n this.ensureInitialized()\n return this.getByPath(this.data, path) as ConfigPathValue<T, P>\n }\n\n /**\n * Check if a config path exists.\n */\n has(path: ConfigPath<T>): boolean {\n this.ensureInitialized()\n return this.getByPath(this.data, path) !== undefined\n }\n\n /**\n * Get the entire config object (readonly snapshot).\n */\n all(): Readonly<T> {\n this.ensureInitialized()\n return this.data as Readonly<T>\n }\n\n /**\n * True once {@link initialize} has been called.\n */\n isInitialized(): boolean {\n return this.data !== undefined\n }\n\n private getByPath(obj: unknown, path: string): unknown {\n const keys = path.split('.')\n let current = obj\n for (const key of keys) {\n if (this.isDangerousKey(key)) return undefined\n if (current === null || current === undefined) return undefined\n current = (current as Record<string, unknown>)[key]\n }\n return current\n }\n\n private isDangerousKey(key: string): boolean {\n return key === '__proto__' || key === 'constructor' || key === 'prototype'\n }\n\n private ensureInitialized(): void {\n if (this.data === undefined) {\n throw new ConfigNotInitializedError()\n }\n }\n\n private deepClone<V>(obj: V): V {\n if (obj === null || typeof obj !== 'object') {\n return obj\n }\n return JSON.parse(JSON.stringify(obj)) as V\n }\n}\n","import { DI_TOKENS } from '../di/tokens'\nimport { Scope } from '../di/types'\nimport type { z } from '../i18n/validation'\nimport { Module } from '../module'\nimport type { DynamicModule, ModuleContext, OnInitialize, Provider } from '../module/types'\nimport { CONFIG_TOKENS } from './config.tokens'\nimport { ConfigValidationError, type ModuleConfig } from './config.types'\nimport { ConfigModuleNotInitializedError } from './errors'\nimport type { ConfigNamespace } from './register-as'\nimport { ConfigService } from './services/config.service'\nimport { ConfigStore } from './services/config.store'\n\n/**\n * Any config namespace - uses structural typing for flexibility\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyConfigNamespace = ConfigNamespace<string, any, object>\n\n/**\n * Options for ConfigModule.forRoot\n */\nexport interface ConfigModuleOptions {\n /**\n * Array of config namespaces created via registerAs()\n */\n load: AnyConfigNamespace[]\n\n /**\n * Optional Zod schema for validating the merged config\n * Validates the entire config object after all namespaces are merged\n */\n validateSchema?: z.ZodType<object>\n}\n\n// Store options for use in onInitialize\nlet moduleOptions: ConfigModuleOptions | null = null\n\n/**\n * ConfigModule\n *\n * Provides configuration management with namespace support.\n * Uses registerAs() to create typed config namespaces that can be injected.\n *\n * @example\n * ```typescript\n * // Define config namespaces\n * const databaseConfig = registerAs('database', (env) => ({\n * url: env.DATABASE_URL,\n * maxConnections: 10\n * }))\n *\n * const emailConfig = registerAs('email', (env) => ({\n * provider: env.EMAIL_PROVIDER,\n * from: { name: 'App', email: 'noreply@example.com' }\n * }))\n *\n * // Register in module\n * @Module({\n * imports: [\n * ConfigModule.forRoot({\n * load: [databaseConfig, emailConfig],\n * validateSchema: AppConfigSchema\n * })\n * ]\n * })\n * export class AppModule {}\n *\n * // Inject config\n * constructor(\n * @inject(CONFIG_TOKENS.ConfigService) private config: IConfigService,\n * @inject(databaseConfig.KEY) private dbConfig: DatabaseConfig\n * ) {\n * // Use dot notation\n * const url = this.config.get('database.url')\n *\n * // Or inject namespace directly\n * const url = this.dbConfig.url\n * }\n * ```\n */\n@Module({\n providers: [\n // ConfigStore is the singleton source of truth for validated config.\n {\n provide: CONFIG_TOKENS.ConfigStore,\n useClass: ConfigStore,\n scope: Scope.Singleton,\n },\n // ConfigService is request-scoped: each request gets its own\n // overrides map layered over the shared ConfigStore.\n {\n provide: CONFIG_TOKENS.ConfigService,\n useClass: ConfigService,\n scope: Scope.Request,\n },\n ],\n})\nexport class ConfigModule implements OnInitialize {\n /**\n * Configure ConfigModule with namespace loaders\n *\n * @param options - Configuration options\n * @returns Dynamic module with config infrastructure\n */\n static forRoot(options: ConfigModuleOptions): DynamicModule {\n moduleOptions = options\n\n const providers: Provider[] = []\n\n // Register each namespace config using asProvider()\n for (const namespace of options.load) {\n providers.push(namespace.asProvider())\n }\n\n return {\n module: ConfigModule,\n providers,\n }\n }\n\n /**\n * Initialize config service with merged namespaces\n * Called after all providers are registered\n */\n onInitialize(context: ModuleContext): void {\n if (!moduleOptions) {\n throw new ConfigModuleNotInitializedError()\n }\n\n const env = context.container.resolve<unknown>(DI_TOKENS.CloudflareEnv)\n const configStore = context.container.resolve<ConfigStore>(CONFIG_TOKENS.ConfigStore)\n\n // Build merged config from all namespaces\n const mergedConfig: Record<string, unknown> = {}\n\n for (const namespace of moduleOptions.load) {\n mergedConfig[namespace.namespace] = namespace.factory(env)\n }\n\n // Validate if schema provided\n if (moduleOptions.validateSchema) {\n const result = moduleOptions.validateSchema.safeParse(mergedConfig)\n if (!result.success) {\n throw new ConfigValidationError(\n 'Configuration validation failed',\n result.error\n )\n }\n }\n\n // Initialize the shared ConfigStore with merged config.\n configStore.initialize(mergedConfig as ModuleConfig)\n\n context.logger.debug('ConfigModule initialized', {\n namespaces: moduleOptions.load.map((n) => n.namespace),\n })\n }\n}\n","import type { InjectionToken } from 'tsyringe'\nimport { DI_TOKENS } from '../di/tokens'\nimport type { FactoryProvider } from '../module/types'\n\n/**\n * Configuration namespace registration result\n */\nexport interface ConfigNamespace<TKey extends string, TEnv, TConfig extends object> {\n /** Auto-derived injection token (e.g., 'database' -> Symbol('stratal:config:database')) */\n readonly KEY: InjectionToken<TConfig>\n /** The namespace key */\n readonly namespace: TKey\n /** The factory function that receives env and returns config */\n readonly factory: (env: TEnv) => TConfig\n /**\n * Returns a provider configuration for use in module registration\n * Automatically injects DI_TOKENS.CloudflareEnv\n */\n asProvider(): FactoryProvider<TConfig>\n}\n\n/**\n * Create a namespaced configuration factory\n * Similar to NestJS registerAs\n *\n * @param namespace - Configuration namespace (e.g., 'database', 'email')\n * @param factory - Factory function receiving env and returning config object\n * @returns ConfigNamespace with token, factory, and asProvider() method\n *\n * @example\n * ```typescript\n * // apps/backend/src/config/database.config.ts\n * export const databaseConfig = registerAs('database', (env: Env) => ({\n * url: env.DATABASE_URL,\n * maxConnections: parseInt(env.DATABASE_MAX_CONNECTIONS || '10'),\n * }))\n *\n * // Auto-generates: databaseConfig.KEY = Symbol('stratal:config:database')\n *\n * // Usage in module:\n * // Option 1: Manual provider\n * {\n * provide: databaseConfig.KEY,\n * useFactory: databaseConfig.factory,\n * inject: [DI_TOKENS.CloudflareEnv]\n * }\n *\n * // Option 2: asProvider() helper\n * databaseConfig.asProvider()\n * // Returns: { provide: databaseConfig.KEY, useFactory: ..., inject: [DI_TOKENS.CloudflareEnv] }\n * ```\n */\nexport function registerAs<TKey extends string, TEnv, TConfig extends object>(\n namespace: TKey,\n factory: (env: TEnv) => TConfig\n): ConfigNamespace<TKey, TEnv, TConfig> {\n const KEY = Symbol.for(`stratal:config:${namespace}`) as InjectionToken<TConfig>\n\n return {\n KEY,\n namespace,\n factory,\n asProvider(): FactoryProvider<TConfig> {\n return {\n provide: KEY,\n useFactory: factory,\n inject: [DI_TOKENS.CloudflareEnv],\n }\n },\n }\n}\n\n/**\n * Helper to derive config type from registerAs result\n *\n * @example\n * ```typescript\n * const databaseConfig = registerAs('database', (env) => ({ url: env.DATABASE_URL }))\n * type DatabaseConfig = InferConfigType<typeof databaseConfig>\n * // { url: string }\n * ```\n */\nexport type InferConfigType<T> = T extends ConfigNamespace<string, unknown, infer C> ? C : never\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consumer-registry-
|
|
1
|
+
{"version":3,"file":"consumer-registry-B7yUNh0q.d.mts","names":[],"sources":["../src/queue/queue-consumer.ts","../src/queue/consumer-registry.ts"],"mappings":";;AAMA;;;;;UAAiB,YAAA;EAIf;EAFA,EAAA;EAMA;EAJA,SAAA;EAMA;EAJA,IAAA;EAMG;EAJH,OAAA,EAAS,CAAA;EAIK;EAFd,QAAA;IACE,MAAA;IAAA,CACC,GAAA;EAAA;AAAA;;;;;;;;;;;;;;;;;;;UAsBY,cAAA;EAsBQ;;;;;;EAAA,SAfd,YAAA;ECXE;;;;;EDkBX,MAAA,CAAO,OAAA,EAAS,YAAA,CAAa,CAAA,IAAK,OAAA;ECqDD;;;;;;ED7CjC,OAAA,EAAS,KAAA,EAAO,KAAA,EAAO,OAAA,EAAS,YAAA,CAAa,CAAA,IAAK,OAAA;AAAA;;;AAxDpD;;;;;;;;;;;;;;;AAkCA;;;;;;;;;;;;;;;;AAlCA,cC8Ba,gBAAA;EDkBJ;EAAA,QChBC,eAAA;EDwBR;EAAA,QCrBQ,YAAA;EDqBC;;;;;;;ECZT,QAAA,CAAS,QAAA,EAAU,cAAA;;;AAfrB;;;;;;;;EAuCE,YAAA,CAAa,WAAA,WAAsB,cAAA;EAjC3B;;;;;;EAgDR,YAAA,CAAa,WAAA;EAAb;;;;;EASA,eAAA,CAAA;EASiC;;;;;EAAjC,eAAA,CAAA,GAAmB,cAAA;AAAA"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { f as ROUTE_METADATA_KEYS } from "./errors-BdyV5PnY.mjs";
|
|
2
|
+
import { p as Transient } from "./logger-V6Ms3QnQ.mjs";
|
|
3
|
+
//#region src/router/decorators/controller.decorator.ts
|
|
4
|
+
const CONTROLLER_ROUTE_KEY = ROUTE_METADATA_KEYS.CONTROLLER_ROUTE;
|
|
5
|
+
/**
|
|
6
|
+
* Base controller decorator for route registration
|
|
7
|
+
*
|
|
8
|
+
* This is the core controller decorator that handles:
|
|
9
|
+
* - Transient scope registration (request-scoped)
|
|
10
|
+
* - Route metadata storage
|
|
11
|
+
* - Controller options (tags, security schemes, hideFromDocs)
|
|
12
|
+
*
|
|
13
|
+
* @param route - Base route for this controller (e.g., '/api/v1/users')
|
|
14
|
+
* @param options - Optional configuration (tags, security schemes, hideFromDocs)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { Controller } from 'stratal/router'
|
|
19
|
+
*
|
|
20
|
+
* @Controller('/api/v1/users', { tags: ['Users'] })
|
|
21
|
+
* export class UsersController implements IController {
|
|
22
|
+
* // All routes accessible
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
function Controller(route, options) {
|
|
27
|
+
return function(target) {
|
|
28
|
+
Transient()(target);
|
|
29
|
+
Reflect.defineMetadata(CONTROLLER_ROUTE_KEY, route, target);
|
|
30
|
+
if (options) Reflect.defineMetadata(ROUTE_METADATA_KEYS.CONTROLLER_OPTIONS, options, target);
|
|
31
|
+
return target;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get the route from controller class metadata
|
|
36
|
+
*
|
|
37
|
+
* @param target - Controller class or instance
|
|
38
|
+
* @returns Route string or undefined if not set
|
|
39
|
+
*/
|
|
40
|
+
function getControllerRoute(target) {
|
|
41
|
+
const metadataTarget = typeof target === "function" ? target : target.constructor;
|
|
42
|
+
return Reflect.getMetadata(CONTROLLER_ROUTE_KEY, metadataTarget);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get the options from controller class metadata
|
|
46
|
+
*
|
|
47
|
+
* @param target - Controller class or instance
|
|
48
|
+
* @returns Controller options or undefined if not set
|
|
49
|
+
*/
|
|
50
|
+
function getControllerOptions(target) {
|
|
51
|
+
const metadataTarget = typeof target === "function" ? target : target.constructor;
|
|
52
|
+
return Reflect.getMetadata(ROUTE_METADATA_KEYS.CONTROLLER_OPTIONS, metadataTarget);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get the version from controller class metadata
|
|
56
|
+
*
|
|
57
|
+
* @param target - Controller class or instance
|
|
58
|
+
* @returns Version string, array, VERSION_NEUTRAL symbol, or undefined if not set
|
|
59
|
+
*/
|
|
60
|
+
function getControllerVersion(target) {
|
|
61
|
+
return getControllerOptions(target)?.version;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { getControllerVersion as i, getControllerOptions as n, getControllerRoute as r, Controller as t };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=controller.decorator-DQzenvSN.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.decorator-DQzenvSN.mjs","names":[],"sources":["../src/router/decorators/controller.decorator.ts"],"sourcesContent":["import { Transient } from '../../di/decorators'\nimport { type Constructor } from '../../types'\nimport { ROUTE_METADATA_KEYS } from '../constants'\nimport { type ControllerOptions } from '../types'\n\nconst CONTROLLER_ROUTE_KEY = ROUTE_METADATA_KEYS.CONTROLLER_ROUTE\n\n/**\n * Base controller decorator for route registration\n *\n * This is the core controller decorator that handles:\n * - Transient scope registration (request-scoped)\n * - Route metadata storage\n * - Controller options (tags, security schemes, hideFromDocs)\n *\n * @param route - Base route for this controller (e.g., '/api/v1/users')\n * @param options - Optional configuration (tags, security schemes, hideFromDocs)\n *\n * @example\n * ```typescript\n * import { Controller } from 'stratal/router'\n *\n * @Controller('/api/v1/users', { tags: ['Users'] })\n * export class UsersController implements IController {\n * // All routes accessible\n * }\n * ```\n */\nexport function Controller(route: string, options?: ControllerOptions) {\n return function <T extends Constructor>(target: T) {\n // Wrap @Transient (handles @injectable and scope metadata)\n Transient()(target)\n\n // Store route metadata on the class\n Reflect.defineMetadata(CONTROLLER_ROUTE_KEY, route, target)\n\n // Store options metadata if provided\n if (options) {\n Reflect.defineMetadata(ROUTE_METADATA_KEYS.CONTROLLER_OPTIONS, options, target)\n }\n\n return target\n }\n}\n\n/**\n * Get the route from controller class metadata\n *\n * @param target - Controller class or instance\n * @returns Route string or undefined if not set\n */\nexport function getControllerRoute(target: object): string | undefined {\n // Check if target is a class constructor (function) or an instance\n // If class, get metadata from it directly; if instance, get from constructor\n const metadataTarget = typeof target === 'function' ? target : (target as { constructor: object }).constructor\n return Reflect.getMetadata(CONTROLLER_ROUTE_KEY, metadataTarget) as string | undefined\n}\n\n/**\n * Get the options from controller class metadata\n *\n * @param target - Controller class or instance\n * @returns Controller options or undefined if not set\n */\nexport function getControllerOptions(target: object): ControllerOptions | undefined {\n const metadataTarget = typeof target === 'function' ? target : (target as { constructor: object }).constructor\n return Reflect.getMetadata(ROUTE_METADATA_KEYS.CONTROLLER_OPTIONS, metadataTarget) as ControllerOptions | undefined\n}\n\n/**\n * Get the version from controller class metadata\n *\n * @param target - Controller class or instance\n * @returns Version string, array, VERSION_NEUTRAL symbol, or undefined if not set\n */\nexport function getControllerVersion(target: object): ControllerOptions['version'] {\n const options = getControllerOptions(target)\n return options?.version\n}\n"],"mappings":";;;AAKA,MAAM,uBAAuB,oBAAoB;;;;;;;;;;;;;;;;;;;;;;AAuBjD,SAAgB,WAAW,OAAe,SAA6B;AACrE,QAAO,SAAiC,QAAW;AAEjD,aAAW,CAAC,OAAO;AAGnB,UAAQ,eAAe,sBAAsB,OAAO,OAAO;AAG3D,MAAI,QACF,SAAQ,eAAe,oBAAoB,oBAAoB,SAAS,OAAO;AAGjF,SAAO;;;;;;;;;AAUX,SAAgB,mBAAmB,QAAoC;CAGrE,MAAM,iBAAiB,OAAO,WAAW,aAAa,SAAU,OAAmC;AACnG,QAAO,QAAQ,YAAY,sBAAsB,eAAe;;;;;;;;AASlE,SAAgB,qBAAqB,QAA+C;CAClF,MAAM,iBAAiB,OAAO,WAAW,aAAa,SAAU,OAAmC;AACnG,QAAO,QAAQ,YAAY,oBAAoB,oBAAoB,eAAe;;;;;;;;AASpF,SAAgB,qBAAqB,QAA8C;AAEjF,QADgB,qBAAqB,OACvB,EAAE"}
|
package/dist/cron/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { d as ApplicationError } from "../index-
|
|
2
|
-
import { n as CronJob, t as CronManager } from "../cron-manager-
|
|
1
|
+
import { d as ApplicationError } from "../index-D0US0X14.mjs";
|
|
2
|
+
import { n as CronJob, r as RegisteredJob, t as CronManager } from "../cron-manager-BEsH1mjW.mjs";
|
|
3
|
+
import { ScheduledController } from "@cloudflare/workers-types";
|
|
3
4
|
|
|
4
5
|
//#region src/cron/errors/cron-execution.error.d.ts
|
|
5
6
|
/**
|
|
@@ -11,5 +12,5 @@ declare class CronExecutionError extends ApplicationError {
|
|
|
11
12
|
constructor(schedule: string, failedJobsCount: number, jobNames: string);
|
|
12
13
|
}
|
|
13
14
|
//#endregion
|
|
14
|
-
export { CronExecutionError, type CronJob, CronManager };
|
|
15
|
+
export { CronExecutionError, type CronJob, CronManager, type RegisteredJob, type ScheduledController };
|
|
15
16
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/cron/errors/cron-execution.error.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/cron/errors/cron-execution.error.ts"],"mappings":";;;;;;;;;AAQA;cAAa,kBAAA,SAA2B,gBAAA;cAEtC,QAAA,UACA,eAAA,UACA,QAAA;AAAA"}
|
package/dist/cron/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as CronExecutionError, t as CronManager } from "../cron-manager-
|
|
1
|
+
import { n as CronExecutionError, t as CronManager } from "../cron-manager-7Symz_TE.mjs";
|
|
2
2
|
export { CronExecutionError, CronManager };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as ApplicationError, k as ERROR_CODES } from "./errors
|
|
2
|
-
import { a as __decorate, p as Transient } from "./logger-
|
|
1
|
+
import { H as ApplicationError, k as ERROR_CODES } from "./errors-BdyV5PnY.mjs";
|
|
2
|
+
import { a as __decorate, p as Transient } from "./logger-V6Ms3QnQ.mjs";
|
|
3
3
|
//#region src/cron/errors/cron-execution.error.ts
|
|
4
4
|
/**
|
|
5
5
|
* Error thrown when one or more cron jobs fail execution
|
|
@@ -19,51 +19,61 @@ var CronExecutionError = class extends ApplicationError {
|
|
|
19
19
|
//#region src/cron/cron-manager.ts
|
|
20
20
|
let CronManager = class CronManager {
|
|
21
21
|
/**
|
|
22
|
-
* Map of cron expressions to
|
|
22
|
+
* Map of cron expressions to registered job entries
|
|
23
23
|
* Key: Cron expression (e.g., '0 2 * * *')
|
|
24
|
-
* Value: Array of jobs
|
|
24
|
+
* Value: Array of registered jobs (class ref + schedule)
|
|
25
25
|
*/
|
|
26
26
|
jobs = /* @__PURE__ */ new Map();
|
|
27
27
|
/**
|
|
28
|
-
* Register a cron job
|
|
28
|
+
* Register a cron job class
|
|
29
29
|
*
|
|
30
30
|
* Jobs with the same schedule are grouped together and executed
|
|
31
31
|
* sequentially when the trigger fires.
|
|
32
32
|
*
|
|
33
|
-
* @param
|
|
33
|
+
* @param schedule - Cron expression (e.g., '0 2 * * *')
|
|
34
|
+
* @param jobClass - CronJob class constructor (resolved at execution time)
|
|
34
35
|
*/
|
|
35
|
-
registerJob(
|
|
36
|
-
const existing = this.jobs.get(
|
|
37
|
-
existing.push(
|
|
38
|
-
|
|
36
|
+
registerJob(schedule, jobClass) {
|
|
37
|
+
const existing = this.jobs.get(schedule) ?? [];
|
|
38
|
+
existing.push({
|
|
39
|
+
schedule,
|
|
40
|
+
jobClass
|
|
41
|
+
});
|
|
42
|
+
this.jobs.set(schedule, existing);
|
|
39
43
|
}
|
|
40
44
|
/**
|
|
41
45
|
* Execute all jobs matching the triggered cron expression
|
|
42
46
|
*
|
|
47
|
+
* Jobs are resolved from the provided request-scoped container,
|
|
48
|
+
* ensuring dependencies (e.g. database) are properly scoped.
|
|
49
|
+
*
|
|
43
50
|
* Jobs are executed sequentially. If a job fails:
|
|
44
51
|
* - Its onError() hook is called (if defined)
|
|
45
52
|
* - Execution continues with the next job
|
|
46
|
-
* - Errors are collected and
|
|
53
|
+
* - Errors are collected and thrown as CronExecutionError
|
|
47
54
|
*
|
|
48
55
|
* @param controller - Cloudflare ScheduledController
|
|
56
|
+
* @param container - Request-scoped container to resolve jobs from
|
|
49
57
|
*/
|
|
50
|
-
async executeScheduled(controller) {
|
|
58
|
+
async executeScheduled(controller, container) {
|
|
51
59
|
const { cron } = controller;
|
|
52
60
|
const matchingJobs = this.jobs.get(cron) ?? [];
|
|
53
61
|
if (matchingJobs.length === 0) return;
|
|
54
62
|
const errors = [];
|
|
55
|
-
for (const
|
|
56
|
-
const jobName =
|
|
63
|
+
for (const { jobClass } of matchingJobs) {
|
|
64
|
+
const jobName = jobClass.name;
|
|
57
65
|
try {
|
|
58
|
-
|
|
66
|
+
container.register(jobClass, jobClass);
|
|
67
|
+
await container.resolve(jobClass).execute(controller);
|
|
59
68
|
} catch (error) {
|
|
60
69
|
const err = error;
|
|
61
70
|
errors.push({
|
|
62
71
|
job: jobName,
|
|
63
72
|
error: err
|
|
64
73
|
});
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
try {
|
|
75
|
+
const job = container.resolve(jobClass);
|
|
76
|
+
if (job.onError) await job.onError(err, controller);
|
|
67
77
|
} catch {}
|
|
68
78
|
}
|
|
69
79
|
}
|
|
@@ -76,7 +86,7 @@ let CronManager = class CronManager {
|
|
|
76
86
|
* Get all registered jobs for a specific cron expression
|
|
77
87
|
*
|
|
78
88
|
* @param schedule - Cron expression
|
|
79
|
-
* @returns Array of jobs
|
|
89
|
+
* @returns Array of registered jobs, or empty array if none
|
|
80
90
|
*/
|
|
81
91
|
getJobsForSchedule(schedule) {
|
|
82
92
|
return this.jobs.get(schedule) ?? [];
|
|
@@ -104,4 +114,4 @@ CronManager = __decorate([Transient()], CronManager);
|
|
|
104
114
|
//#endregion
|
|
105
115
|
export { CronExecutionError as n, CronManager as t };
|
|
106
116
|
|
|
107
|
-
//# sourceMappingURL=cron-manager-
|
|
117
|
+
//# sourceMappingURL=cron-manager-7Symz_TE.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron-manager-7Symz_TE.mjs","names":[],"sources":["../src/cron/errors/cron-execution.error.ts","../src/cron/cron-manager.ts"],"sourcesContent":["import { ApplicationError } from '../../errors'\nimport { ERROR_CODES } from '../../errors'\n\n/**\n * Error thrown when one or more cron jobs fail execution\n *\n * This error aggregates failures from multiple jobs that share the same schedule.\n */\nexport class CronExecutionError extends ApplicationError {\n\tconstructor(\n\t\tschedule: string,\n\t\tfailedJobsCount: number,\n\t\tjobNames: string\n\t) {\n\t\tsuper(\n\t\t\t'errors.cronExecutionFailed',\n\t\t\tERROR_CODES.SYSTEM.CRON_EXECUTION_FAILED,\n\t\t\t{\n\t\t\t\tschedule,\n\t\t\t\tcount: failedJobsCount,\n\t\t\t\tjobs: jobNames\n\t\t\t}\n\t\t)\n\t}\n}\n","import type { Container } from '../di/container'\nimport { Transient } from '../di/decorators'\nimport type { CronJob, RegisteredJob } from './cron-job'\nimport { CronExecutionError } from './errors/cron-execution.error'\n\n/**\n * Manages cron job registration and execution\n *\n * CronManager is a singleton service that:\n * - Registers cron job class references from modules\n * - Routes scheduled events to matching jobs\n * - Resolves jobs from a request-scoped container at execution time\n *\n * Jobs are grouped by their cron expression, allowing multiple jobs\n * to run on the same schedule.\n */\n@Transient()\nexport class CronManager {\n\t/**\n\t * Map of cron expressions to registered job entries\n\t * Key: Cron expression (e.g., '0 2 * * *')\n\t * Value: Array of registered jobs (class ref + schedule)\n\t */\n\tprivate jobs = new Map<string, RegisteredJob[]>()\n\n\t/**\n\t * Register a cron job class\n\t *\n\t * Jobs with the same schedule are grouped together and executed\n\t * sequentially when the trigger fires.\n\t *\n\t * @param schedule - Cron expression (e.g., '0 2 * * *')\n\t * @param jobClass - CronJob class constructor (resolved at execution time)\n\t */\n\tregisterJob(schedule: string, jobClass: RegisteredJob['jobClass']): void {\n\t\tconst existing = this.jobs.get(schedule) ?? []\n\t\texisting.push({ schedule, jobClass })\n\t\tthis.jobs.set(schedule, existing)\n\t}\n\n\t/**\n\t * Execute all jobs matching the triggered cron expression\n\t *\n\t * Jobs are resolved from the provided request-scoped container,\n\t * ensuring dependencies (e.g. database) are properly scoped.\n\t *\n\t * Jobs are executed sequentially. If a job fails:\n\t * - Its onError() hook is called (if defined)\n\t * - Execution continues with the next job\n\t * - Errors are collected and thrown as CronExecutionError\n\t *\n\t * @param controller - Cloudflare ScheduledController\n\t * @param container - Request-scoped container to resolve jobs from\n\t */\n\tasync executeScheduled(controller: ScheduledController, container: Container): Promise<void> {\n\t\tconst { cron } = controller\n\t\tconst matchingJobs = this.jobs.get(cron) ?? []\n\n\t\tif (matchingJobs.length === 0) {\n\t\t\treturn\n\t\t}\n\n\t\tconst errors: { job: string; error: Error }[] = []\n\n\t\tfor (const { jobClass } of matchingJobs) {\n\t\t\tconst jobName = jobClass.name\n\n\t\t\ttry {\n\t\t\t\t// Register the job class in the request-scoped container so its\n\t\t\t\t// dependencies are resolved from request scope (not the parent).\n\t\t\t\t// Without this, tsyringe falls through to the parent container\n\t\t\t\t// and request-scoped services (e.g. database) get stale instances.\n\t\t\t\tcontainer.register(jobClass, jobClass)\n\t\t\t\tconst job = container.resolve<CronJob>(jobClass)\n\t\t\t\tawait job.execute(controller)\n\t\t\t} catch (error) {\n\t\t\t\tconst err = error as Error\n\t\t\t\terrors.push({ job: jobName, error: err })\n\n\t\t\t\t// Try to resolve and call onError if possible\n\t\t\t\ttry {\n\t\t\t\t\tconst job = container.resolve<CronJob>(jobClass)\n\t\t\t\t\tif (job.onError) {\n\t\t\t\t\t\tawait job.onError(err, controller)\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// If resolution or onError fails, continue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// If any jobs failed, throw an aggregate error\n\t\t// This ensures the error is logged by ExceptionHandler\n\t\tif (errors.length > 0) {\n\t\t\tconst jobNames = errors\n\t\t\t\t.map(({ job, error }) => `${job}: ${error.message}`)\n\t\t\t\t.join('; ')\n\n\t\t\tthrow new CronExecutionError(cron, errors.length, jobNames)\n\t\t}\n\t}\n\n\t/**\n\t * Get all registered jobs for a specific cron expression\n\t *\n\t * @param schedule - Cron expression\n\t * @returns Array of registered jobs, or empty array if none\n\t */\n\tgetJobsForSchedule(schedule: string): RegisteredJob[] {\n\t\treturn this.jobs.get(schedule) ?? []\n\t}\n\n\t/**\n\t * Get all registered cron expressions\n\t *\n\t * @returns Array of unique cron expressions\n\t */\n\tgetAllSchedules(): string[] {\n\t\treturn Array.from(this.jobs.keys())\n\t}\n\n\t/**\n\t * Get total number of registered jobs across all schedules\n\t *\n\t * @returns Total job count\n\t */\n\tgetTotalJobCount(): number {\n\t\tlet count = 0\n\t\tfor (const jobs of this.jobs.values()) {\n\t\t\tcount += jobs.length\n\t\t}\n\t\treturn count\n\t}\n}\n"],"mappings":";;;;;;;;AAQA,IAAa,qBAAb,cAAwC,iBAAiB;CACxD,YACC,UACA,iBACA,UACC;AACD,QACC,8BACA,YAAY,OAAO,uBACnB;GACC;GACA,OAAO;GACP,MAAM;GACN,CACD;;;;;ACLI,IAAA,cAAA,MAAM,YAAY;;;;;;CAMxB,uBAAe,IAAI,KAA8B;;;;;;;;;;CAWjD,YAAY,UAAkB,UAA2C;EACxE,MAAM,WAAW,KAAK,KAAK,IAAI,SAAS,IAAI,EAAE;AAC9C,WAAS,KAAK;GAAE;GAAU;GAAU,CAAC;AACrC,OAAK,KAAK,IAAI,UAAU,SAAS;;;;;;;;;;;;;;;;CAiBlC,MAAM,iBAAiB,YAAiC,WAAqC;EAC5F,MAAM,EAAE,SAAS;EACjB,MAAM,eAAe,KAAK,KAAK,IAAI,KAAK,IAAI,EAAE;AAE9C,MAAI,aAAa,WAAW,EAC3B;EAGD,MAAM,SAA0C,EAAE;AAElD,OAAK,MAAM,EAAE,cAAc,cAAc;GACxC,MAAM,UAAU,SAAS;AAEzB,OAAI;AAKH,cAAU,SAAS,UAAU,SAAS;AAEtC,UADY,UAAU,QAAiB,SAC9B,CAAC,QAAQ,WAAW;YACrB,OAAO;IACf,MAAM,MAAM;AACZ,WAAO,KAAK;KAAE,KAAK;KAAS,OAAO;KAAK,CAAC;AAGzC,QAAI;KACH,MAAM,MAAM,UAAU,QAAiB,SAAS;AAChD,SAAI,IAAI,QACP,OAAM,IAAI,QAAQ,KAAK,WAAW;YAE5B;;;AAQV,MAAI,OAAO,SAAS,GAAG;GACtB,MAAM,WAAW,OACf,KAAK,EAAE,KAAK,YAAY,GAAG,IAAI,IAAI,MAAM,UAAU,CACnD,KAAK,KAAK;AAEZ,SAAM,IAAI,mBAAmB,MAAM,OAAO,QAAQ,SAAS;;;;;;;;;CAU7D,mBAAmB,UAAmC;AACrD,SAAO,KAAK,KAAK,IAAI,SAAS,IAAI,EAAE;;;;;;;CAQrC,kBAA4B;AAC3B,SAAO,MAAM,KAAK,KAAK,KAAK,MAAM,CAAC;;;;;;;CAQpC,mBAA2B;EAC1B,IAAI,QAAQ;AACZ,OAAK,MAAM,QAAQ,KAAK,KAAK,QAAQ,CACpC,UAAS,KAAK;AAEf,SAAO;;;0BAnHR,WAAW,CAAA,EAAA,YAAA"}
|