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.
Files changed (185) hide show
  1. package/README.md +8 -8
  2. package/dist/{base-email.provider-Cuw4OAB0.mjs → base-email.provider-CfQCA08m.mjs} +1 -1
  3. package/dist/{base-email.provider-Cuw4OAB0.mjs.map → base-email.provider-CfQCA08m.mjs.map} +1 -1
  4. package/dist/bin/cloudflare-workers-loader.mjs.map +1 -1
  5. package/dist/bin/quarry.mjs +26 -35
  6. package/dist/bin/quarry.mjs.map +1 -1
  7. package/dist/cache/index.d.mts +2 -153
  8. package/dist/cache/index.d.mts.map +1 -1
  9. package/dist/cache/index.mjs +4 -6
  10. package/dist/cache/index.mjs.map +1 -1
  11. package/dist/cache.service-DsnKuNyO.d.mts +156 -0
  12. package/dist/cache.service-DsnKuNyO.d.mts.map +1 -0
  13. package/dist/cache.tokens-B7Rw1C9Q.mjs +6 -0
  14. package/dist/cache.tokens-B7Rw1C9Q.mjs.map +1 -0
  15. package/dist/{colors-BTAnQRGU.mjs → colors-DJaRDXoS.mjs} +1 -1
  16. package/dist/{colors-BTAnQRGU.mjs.map → colors-DJaRDXoS.mjs.map} +1 -1
  17. package/dist/{command-DjGqCYHv.mjs → command-BgSlsS4M.mjs} +2 -2
  18. package/dist/{command-DjGqCYHv.mjs.map → command-BgSlsS4M.mjs.map} +1 -1
  19. package/dist/{command-B1YuV-UZ.d.mts → command-Bu-PjJrX.d.mts} +2 -2
  20. package/dist/{command-B1YuV-UZ.d.mts.map → command-Bu-PjJrX.d.mts.map} +1 -1
  21. package/dist/config/index.d.mts +81 -37
  22. package/dist/config/index.d.mts.map +1 -1
  23. package/dist/config/index.mjs +126 -45
  24. package/dist/config/index.mjs.map +1 -1
  25. package/dist/{consumer-registry-BkuHXR_u.d.mts → consumer-registry-B7yUNh0q.d.mts} +1 -1
  26. package/dist/{consumer-registry-BkuHXR_u.d.mts.map → consumer-registry-B7yUNh0q.d.mts.map} +1 -1
  27. package/dist/controller.decorator-DQzenvSN.mjs +66 -0
  28. package/dist/controller.decorator-DQzenvSN.mjs.map +1 -0
  29. package/dist/cron/index.d.mts +4 -3
  30. package/dist/cron/index.d.mts.map +1 -1
  31. package/dist/cron/index.mjs +1 -1
  32. package/dist/{cron-manager-1KnZvojs.mjs → cron-manager-7Symz_TE.mjs} +29 -19
  33. package/dist/cron-manager-7Symz_TE.mjs.map +1 -0
  34. package/dist/{cron-manager-BnEZquBL.d.mts → cron-manager-BEsH1mjW.d.mts} +27 -13
  35. package/dist/cron-manager-BEsH1mjW.d.mts.map +1 -0
  36. package/dist/di/index.d.mts +1 -1
  37. package/dist/di/index.mjs +2 -2
  38. package/dist/email/index.d.mts +3 -3
  39. package/dist/email/index.mjs +87 -10
  40. package/dist/email/index.mjs.map +1 -1
  41. package/dist/{en-3QnZwP-u.mjs → en-DSH_bhh6.mjs} +10 -30
  42. package/dist/en-DSH_bhh6.mjs.map +1 -0
  43. package/dist/env-D1rcZ8_r.d.mts +25 -0
  44. package/dist/env-D1rcZ8_r.d.mts.map +1 -0
  45. package/dist/errors/index.d.mts +1 -1
  46. package/dist/errors/index.mjs +1 -1
  47. package/dist/{errors--RBIvDXr.mjs → errors-BdyV5PnY.mjs} +180 -15
  48. package/dist/errors-BdyV5PnY.mjs.map +1 -0
  49. package/dist/{errors-B7hCnXgB.mjs → errors-Da3Pz2X7.mjs} +14 -7
  50. package/dist/errors-Da3Pz2X7.mjs.map +1 -0
  51. package/dist/events/index.d.mts +2 -2
  52. package/dist/events/index.mjs +1 -1
  53. package/dist/{events-UTJliZhl.mjs → events-COKixqnG.mjs} +2 -2
  54. package/dist/{events-UTJliZhl.mjs.map → events-COKixqnG.mjs.map} +1 -1
  55. package/dist/{gateway-context-BdBFoQd8.mjs → gateway-context-CdJjpUCW.mjs} +5 -70
  56. package/dist/gateway-context-CdJjpUCW.mjs.map +1 -0
  57. package/dist/guards/index.d.mts +14 -5
  58. package/dist/guards/index.d.mts.map +1 -1
  59. package/dist/guards/index.mjs +1 -1
  60. package/dist/{guards-MtDgcHnF.mjs → guards-DUk_Kzst.mjs} +1 -1
  61. package/dist/guards-DUk_Kzst.mjs.map +1 -0
  62. package/dist/http-method.decorator-DXwxAfb_.mjs +96 -0
  63. package/dist/http-method.decorator-DXwxAfb_.mjs.map +1 -0
  64. package/dist/i18n/index.d.mts +3 -3
  65. package/dist/i18n/index.mjs +2 -2
  66. package/dist/i18n/messages/en/index.d.mts +1 -1
  67. package/dist/i18n/messages/en/index.mjs +1 -1
  68. package/dist/i18n/utils/index.mjs +1 -1
  69. package/dist/i18n/validation/index.d.mts +2 -2
  70. package/dist/i18n/validation/index.mjs +2 -2
  71. package/dist/{i18n.module-BpLLLCTg.mjs → i18n.module-BBlNNlcG.mjs} +234 -204
  72. package/dist/i18n.module-BBlNNlcG.mjs.map +1 -0
  73. package/dist/index-7-hU3GTV.d.mts +101 -0
  74. package/dist/index-7-hU3GTV.d.mts.map +1 -0
  75. package/dist/{index-Dfpd_ypO.d.mts → index-Bnpfq6uk.d.mts} +81 -19
  76. package/dist/index-Bnpfq6uk.d.mts.map +1 -0
  77. package/dist/{index-BDh9J2KD.d.mts → index-C1KvMncZ.d.mts} +9 -29
  78. package/dist/{index-BDh9J2KD.d.mts.map → index-C1KvMncZ.d.mts.map} +1 -1
  79. package/dist/{index-DPxmo6AY.d.mts → index-CjaQ6_tZ.d.mts} +5 -4
  80. package/dist/index-CjaQ6_tZ.d.mts.map +1 -0
  81. package/dist/{index-BrmS34sa.d.mts → index-D0US0X14.d.mts} +375 -235
  82. package/dist/index-D0US0X14.d.mts.map +1 -0
  83. package/dist/{index-BR23zDMy.d.mts → index-DBd_2wv8.d.mts} +1 -1
  84. package/dist/{index-BR23zDMy.d.mts.map → index-DBd_2wv8.d.mts.map} +1 -1
  85. package/dist/index.d.mts +3 -2
  86. package/dist/index.d.mts.map +1 -1
  87. package/dist/index.mjs +1 -1
  88. package/dist/{is-command-PvULqiTa.mjs → is-command-C6a7WTPw.mjs} +2 -2
  89. package/dist/{is-command-PvULqiTa.mjs.map → is-command-C6a7WTPw.mjs.map} +1 -1
  90. package/dist/{is-seeder-BN9Ej1r7.mjs → is-seeder-CebjZCDn.mjs} +1 -1
  91. package/dist/{is-seeder-BN9Ej1r7.mjs.map → is-seeder-CebjZCDn.mjs.map} +1 -1
  92. package/dist/logger/index.d.mts +1 -1
  93. package/dist/logger/index.mjs +1 -1
  94. package/dist/{logger-c0ftIK4G.mjs → logger-V6Ms3QnQ.mjs} +38 -20
  95. package/dist/{logger-c0ftIK4G.mjs.map → logger-V6Ms3QnQ.mjs.map} +1 -1
  96. package/dist/macroable/index.d.mts +2 -0
  97. package/dist/macroable/index.mjs +2 -0
  98. package/dist/macroable-BmufBshB.mjs +122 -0
  99. package/dist/macroable-BmufBshB.mjs.map +1 -0
  100. package/dist/module/index.d.mts +2 -2
  101. package/dist/module/index.mjs +1 -1
  102. package/dist/{module-C3YZ-kZN.mjs → module-Dk2qTa77.mjs} +160 -19
  103. package/dist/module-Dk2qTa77.mjs.map +1 -0
  104. package/dist/openapi/index.d.mts +3 -3
  105. package/dist/openapi/index.mjs +2 -2
  106. package/dist/{openapi-tools.service-B77QXD56.mjs → openapi-tools.service-Zs-Ewv7F.mjs} +4 -1
  107. package/dist/{openapi-tools.service-B77QXD56.mjs.map → openapi-tools.service-Zs-Ewv7F.mjs.map} +1 -1
  108. package/dist/{openapi.service-6yj0BUY4.d.mts → openapi.service-BLgvn3hJ.d.mts} +3 -3
  109. package/dist/{openapi.service-6yj0BUY4.d.mts.map → openapi.service-BLgvn3hJ.d.mts.map} +1 -1
  110. package/dist/quarry/index.d.mts +7 -7
  111. package/dist/quarry/index.d.mts.map +1 -1
  112. package/dist/quarry/index.mjs +4 -4
  113. package/dist/{quarry-registry-CQCIlYTO.mjs → quarry-registry-DNEej-Db.mjs} +17 -15
  114. package/dist/quarry-registry-DNEej-Db.mjs.map +1 -0
  115. package/dist/queue/index.d.mts +2 -2
  116. package/dist/queue/index.mjs +2 -2
  117. package/dist/{queue.module-DIjD6nr-.mjs → queue.module-BCdCiySt.mjs} +4 -4
  118. package/dist/{queue.module-DIjD6nr-.mjs.map → queue.module-BCdCiySt.mjs.map} +1 -1
  119. package/dist/r2-storage.provider-Co6F0ZYV.mjs +244 -0
  120. package/dist/r2-storage.provider-Co6F0ZYV.mjs.map +1 -0
  121. package/dist/rate-limit.decorator--o6Q6p9w.mjs +55 -0
  122. package/dist/rate-limit.decorator--o6Q6p9w.mjs.map +1 -0
  123. package/dist/rate-limiter/index.d.mts +420 -0
  124. package/dist/rate-limiter/index.d.mts.map +1 -0
  125. package/dist/rate-limiter/index.mjs +365 -0
  126. package/dist/rate-limiter/index.mjs.map +1 -0
  127. package/dist/{resend.provider-Bvw36rQy.mjs → resend.provider-M6qRLrcy.mjs} +2 -2
  128. package/dist/{resend.provider-Bvw36rQy.mjs.map → resend.provider-M6qRLrcy.mjs.map} +1 -1
  129. package/dist/router/index.d.mts +2 -2
  130. package/dist/router/index.mjs +7 -5
  131. package/dist/seeder/index.d.mts +3 -3
  132. package/dist/seeder/index.mjs +2 -2
  133. package/dist/{seeder-D7VXULXB.mjs → seeder-CJAOHEIo.mjs} +5 -5
  134. package/dist/{seeder-D7VXULXB.mjs.map → seeder-CJAOHEIo.mjs.map} +1 -1
  135. package/dist/{setup-BRIN-iYT.mjs → setup-CefZKV_e.mjs} +1 -1
  136. package/dist/{setup-BRIN-iYT.mjs.map → setup-CefZKV_e.mjs.map} +1 -1
  137. package/dist/signed-url-BQPbv2In.mjs +74 -0
  138. package/dist/signed-url-BQPbv2In.mjs.map +1 -0
  139. package/dist/{smtp.provider-CAwpvzvD.mjs → smtp.provider-w0Ve52Xg.mjs} +2 -2
  140. package/dist/{smtp.provider-CAwpvzvD.mjs.map → smtp.provider-w0Ve52Xg.mjs.map} +1 -1
  141. package/dist/storage/index.d.mts +39 -17
  142. package/dist/storage/index.d.mts.map +1 -1
  143. package/dist/storage/index.mjs +3 -3
  144. package/dist/storage/providers/index.d.mts +30 -70
  145. package/dist/storage/providers/index.d.mts.map +1 -1
  146. package/dist/storage/providers/index.mjs +2 -2
  147. package/dist/{storage-CJ-QOwNv.mjs → storage-1zw-6Yiz.mjs} +101 -27
  148. package/dist/storage-1zw-6Yiz.mjs.map +1 -0
  149. package/dist/{storage-provider.interface-YRtyYBxV.d.mts → storage-provider.interface-Bd6vA4ak.d.mts} +20 -21
  150. package/dist/storage-provider.interface-Bd6vA4ak.d.mts.map +1 -0
  151. package/dist/{stratal-B7G4i9-N.mjs → stratal-DeEcGgdq.mjs} +57 -26
  152. package/dist/stratal-DeEcGgdq.mjs.map +1 -0
  153. package/dist/{types-CN0zONAZ.d.mts → types-cySNS_lp.d.mts} +1 -1
  154. package/dist/types-cySNS_lp.d.mts.map +1 -0
  155. package/dist/{usage-generator-Cl1HPlUp.mjs → usage-generator-BUdlhnCK.mjs} +2 -2
  156. package/dist/{usage-generator-Cl1HPlUp.mjs.map → usage-generator-BUdlhnCK.mjs.map} +1 -1
  157. package/dist/{validation-B4bePOa_.mjs → validation-DtJwAv7O.mjs} +62 -8
  158. package/dist/validation-DtJwAv7O.mjs.map +1 -0
  159. package/dist/websocket/index.d.mts +9 -4
  160. package/dist/websocket/index.d.mts.map +1 -1
  161. package/dist/websocket/index.mjs +1 -1
  162. package/dist/workers/index.d.mts +2 -1
  163. package/dist/workers/index.d.mts.map +1 -1
  164. package/dist/workers/index.mjs +2 -2
  165. package/package.json +32 -40
  166. package/dist/cron-manager-1KnZvojs.mjs.map +0 -1
  167. package/dist/cron-manager-BnEZquBL.d.mts.map +0 -1
  168. package/dist/en-3QnZwP-u.mjs.map +0 -1
  169. package/dist/errors--RBIvDXr.mjs.map +0 -1
  170. package/dist/errors-B7hCnXgB.mjs.map +0 -1
  171. package/dist/gateway-context-BdBFoQd8.mjs.map +0 -1
  172. package/dist/guards-MtDgcHnF.mjs.map +0 -1
  173. package/dist/i18n.module-BpLLLCTg.mjs.map +0 -1
  174. package/dist/index-BrmS34sa.d.mts.map +0 -1
  175. package/dist/index-DPxmo6AY.d.mts.map +0 -1
  176. package/dist/index-Dfpd_ypO.d.mts.map +0 -1
  177. package/dist/module-C3YZ-kZN.mjs.map +0 -1
  178. package/dist/quarry-registry-CQCIlYTO.mjs.map +0 -1
  179. package/dist/s3-storage.provider-BAhHDMI3.mjs +0 -343
  180. package/dist/s3-storage.provider-BAhHDMI3.mjs.map +0 -1
  181. package/dist/storage-CJ-QOwNv.mjs.map +0 -1
  182. package/dist/storage-provider.interface-YRtyYBxV.d.mts.map +0 -1
  183. package/dist/stratal-B7G4i9-N.mjs.map +0 -1
  184. package/dist/types-CN0zONAZ.d.mts.map +0 -1
  185. package/dist/validation-B4bePOa_.mjs.map +0 -1
@@ -1,8 +1,13 @@
1
- import { A as Scope, H as ApplicationError, k as ERROR_CODES } from "../errors--RBIvDXr.mjs";
2
- import { a as __decorate, f as DI_TOKENS, p as Transient } from "../logger-c0ftIK4G.mjs";
3
- import { S as Module } from "../module-C3YZ-kZN.mjs";
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 = { ConfigService: Symbol.for("stratal:config:service") };
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
- originalConfig;
45
- currentConfig;
46
- /**
47
- * Initialize the config service with validated configuration
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
- * @example config.get('database.url')
55
+ * Get config value using dot notation. Request overrides take
56
+ * precedence over the shared store.
57
57
  */
58
58
  get(path) {
59
- this.ensureInitialized();
60
- return this.getByPath(this.currentConfig, path);
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 at runtime (for runtime overrides)
64
- * @example config.set('email.from.name', 'Custom Name')
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.ensureInitialized();
68
- this.setByPath(this.currentConfig, path, value);
68
+ if (this.hasDangerousSegment(path)) return;
69
+ this.overrides.set(path, value);
69
70
  }
70
71
  /**
71
- * Reset config to original value
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
- const originalValue = this.getByPath(this.originalConfig, path);
78
- this.setByPath(this.currentConfig, path, this.deepClone(originalValue));
79
- } else this.currentConfig = this.deepClone(this.originalConfig);
76
+ this.overrides.delete(path);
77
+ return;
78
+ }
79
+ this.overrides.clear();
80
80
  }
81
81
  /**
82
- * Get entire config object
82
+ * Get the full config object, with request overrides merged in.
83
83
  */
84
84
  all() {
85
- this.ensureInitialized();
86
- return this.currentConfig;
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.ensureInitialized();
93
- return this.getByPath(this.currentConfig, path) !== void 0;
95
+ if (this.readOverride(path) !== void 0) return true;
96
+ return this.store.has(path);
94
97
  }
95
- getByPath(obj, path) {
96
- const keys = path.split(".");
97
- let current = obj;
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
- setByPath(obj, path, value) {
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 (!this.currentConfig) throw new ConfigNotInitializedError();
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
- ConfigService = __decorate([Transient(CONFIG_TOKENS.ConfigService)], ConfigService);
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 configService = context.container.resolve(CONFIG_TOKENS.ConfigService);
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
- configService.initialize(mergedConfig);
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.Singleton
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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"}
@@ -139,4 +139,4 @@ declare class ConsumerRegistry {
139
139
  }
140
140
  //#endregion
141
141
  export { IQueueConsumer as n, QueueMessage as r, ConsumerRegistry as t };
142
- //# sourceMappingURL=consumer-registry-BkuHXR_u.d.mts.map
142
+ //# sourceMappingURL=consumer-registry-B7yUNh0q.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"consumer-registry-BkuHXR_u.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"}
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"}
@@ -1,5 +1,6 @@
1
- import { d as ApplicationError } from "../index-BrmS34sa.mjs";
2
- import { n as CronJob, t as CronManager } from "../cron-manager-BnEZquBL.mjs";
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":";;;;;;;AAQA;;cAAa,kBAAA,SAA2B,gBAAA;cAEtC,QAAA,UACA,eAAA,UACA,QAAA;AAAA"}
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"}
@@ -1,2 +1,2 @@
1
- import { n as CronExecutionError, t as CronManager } from "../cron-manager-1KnZvojs.mjs";
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--RBIvDXr.mjs";
2
- import { a as __decorate, p as Transient } from "./logger-c0ftIK4G.mjs";
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 jobs
22
+ * Map of cron expressions to registered job entries
23
23
  * Key: Cron expression (e.g., '0 2 * * *')
24
- * Value: Array of jobs matching that expression
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 job - CronJob instance to register
33
+ * @param schedule - Cron expression (e.g., '0 2 * * *')
34
+ * @param jobClass - CronJob class constructor (resolved at execution time)
34
35
  */
35
- registerJob(job) {
36
- const existing = this.jobs.get(job.schedule) ?? [];
37
- existing.push(job);
38
- this.jobs.set(job.schedule, existing);
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 logged
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 job of matchingJobs) {
56
- const jobName = job.constructor.name;
63
+ for (const { jobClass } of matchingJobs) {
64
+ const jobName = jobClass.name;
57
65
  try {
58
- await job.execute(controller);
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
- if (job.onError) try {
66
- await job.onError(err, controller);
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 for that schedule, or empty array if none
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-1KnZvojs.mjs.map
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"}