@zerotal/core 1.0.0

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 (201) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +128 -0
  4. package/package.json +72 -0
  5. package/src/application/Application.ts +1671 -0
  6. package/src/application/BootDoctor.ts +108 -0
  7. package/src/application/DevErrorPage.ts +567 -0
  8. package/src/application/ExceptionHandler.ts +183 -0
  9. package/src/application/currentApp.ts +73 -0
  10. package/src/assets/assets.ts +79 -0
  11. package/src/assets/index.ts +16 -0
  12. package/src/auth/AuthenticatedUser.ts +18 -0
  13. package/src/build/PackageLinter.ts +146 -0
  14. package/src/build/PackageScaffold.ts +127 -0
  15. package/src/build/codemod.ts +64 -0
  16. package/src/build/index.ts +12 -0
  17. package/src/command/Command.ts +254 -0
  18. package/src/command/CommandRunner.ts +593 -0
  19. package/src/command/OutputWriter.ts +61 -0
  20. package/src/command/builtin/CompileCommand.ts +46 -0
  21. package/src/command/builtin/CssBuildCommand.ts +71 -0
  22. package/src/command/builtin/KeyGenerateCommand.ts +58 -0
  23. package/src/command/builtin/LintPackagesCommand.ts +72 -0
  24. package/src/command/builtin/MakeCommandCommand.ts +85 -0
  25. package/src/command/builtin/MakeControllerCommand.ts +95 -0
  26. package/src/command/builtin/MakeEventCommand.ts +85 -0
  27. package/src/command/builtin/MakeJobCommand.ts +53 -0
  28. package/src/command/builtin/MakeListenerCommand.ts +35 -0
  29. package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
  30. package/src/command/builtin/MakeNotificationCommand.ts +48 -0
  31. package/src/command/builtin/MakeObserverCommand.ts +78 -0
  32. package/src/command/builtin/MakePackageCommand.ts +45 -0
  33. package/src/command/builtin/MakePolicyCommand.ts +66 -0
  34. package/src/command/builtin/MakeProviderCommand.ts +75 -0
  35. package/src/command/builtin/MakeRequestCommand.ts +47 -0
  36. package/src/command/builtin/MakeResourceCommand.ts +61 -0
  37. package/src/command/builtin/MakeTestCommand.ts +120 -0
  38. package/src/command/builtin/ReloadCommand.ts +52 -0
  39. package/src/command/builtin/ReplCommand.ts +174 -0
  40. package/src/command/builtin/RouteListCommand.ts +188 -0
  41. package/src/command/builtin/ServeCommand.ts +321 -0
  42. package/src/command/builtin/StartCommand.ts +3 -0
  43. package/src/command/builtin/StatusCommand.ts +71 -0
  44. package/src/command/builtin/TestCommand.ts +172 -0
  45. package/src/command/builtin/WorkerCommand.ts +27 -0
  46. package/src/command/builtin/index.ts +53 -0
  47. package/src/command/scaffold/worker.ts.txt +12 -0
  48. package/src/command/scaffold/zerotal.ts.txt +26 -0
  49. package/src/command/startZerotal.ts +55 -0
  50. package/src/config/AppConfig.ts +253 -0
  51. package/src/config/ConfigLoader.ts +117 -0
  52. package/src/config/ConfigManager.ts +169 -0
  53. package/src/config/index.ts +46 -0
  54. package/src/config/registry.ts +59 -0
  55. package/src/config/validation.ts +117 -0
  56. package/src/container/Container.ts +606 -0
  57. package/src/container/ContextualBindingBuilder.ts +57 -0
  58. package/src/container/ScopedResolver.ts +117 -0
  59. package/src/container/index.ts +32 -0
  60. package/src/container/inject.ts +55 -0
  61. package/src/container/types.ts +71 -0
  62. package/src/context/RequestContext.ts +91 -0
  63. package/src/contracts/auth.ts +24 -0
  64. package/src/contracts/index.ts +23 -0
  65. package/src/contracts/session.ts +70 -0
  66. package/src/contracts/transaction.ts +26 -0
  67. package/src/conventions/ConventionLoader.ts +128 -0
  68. package/src/conventions/builtinConcerns.ts +131 -0
  69. package/src/crypt/Crypt.ts +141 -0
  70. package/src/crypt/URLSigner.ts +96 -0
  71. package/src/datetime/Carbon.ts +1396 -0
  72. package/src/datetime/CarbonInterval.ts +421 -0
  73. package/src/datetime/clock.ts +28 -0
  74. package/src/datetime/index.ts +23 -0
  75. package/src/datetime/temporal-shim.ts +1 -0
  76. package/src/dev/BuildOutput.ts +131 -0
  77. package/src/dev/CssPlugins.ts +184 -0
  78. package/src/dev/DevBuildHook.ts +74 -0
  79. package/src/dev/DevOrchestrator.ts +213 -0
  80. package/src/dev/DevReloadMiddleware.ts +101 -0
  81. package/src/dev/DevReloadServer.ts +85 -0
  82. package/src/dev/DevWsServer.ts +45 -0
  83. package/src/dev/index.ts +19 -0
  84. package/src/dev/reloadClient.ts +39 -0
  85. package/src/env/Def.ts +232 -0
  86. package/src/env/EnvSchema.ts +105 -0
  87. package/src/env/index.ts +34 -0
  88. package/src/env/t.ts +128 -0
  89. package/src/errors/ConfigError.ts +12 -0
  90. package/src/errors/ContainerErrors.ts +143 -0
  91. package/src/errors/HttpError.ts +127 -0
  92. package/src/errors/ValidationError.ts +19 -0
  93. package/src/errors/ZerotalError.ts +25 -0
  94. package/src/errors/index.ts +46 -0
  95. package/src/events/CallQueuedListener.ts +66 -0
  96. package/src/events/Emitter.ts +280 -0
  97. package/src/events/EventFake.ts +160 -0
  98. package/src/events/FrameworkEvents.ts +252 -0
  99. package/src/facade/Facade.ts +101 -0
  100. package/src/facade/facades/App.ts +155 -0
  101. package/src/facade/facades/Artisan.ts +63 -0
  102. package/src/facade/facades/Config.ts +21 -0
  103. package/src/facade/facades/Events.ts +19 -0
  104. package/src/facade/facades/index.ts +28 -0
  105. package/src/global.d.ts +9 -0
  106. package/src/hash/Hash.ts +60 -0
  107. package/src/health/Health.ts +221 -0
  108. package/src/health/index.ts +27 -0
  109. package/src/helpers/Collection.ts +435 -0
  110. package/src/helpers/config.ts +59 -0
  111. package/src/helpers/fluent.ts +52 -0
  112. package/src/helpers/html.ts +11 -0
  113. package/src/helpers/index.ts +266 -0
  114. package/src/helpers/make.ts +35 -0
  115. package/src/helpers/markdown.ts +73 -0
  116. package/src/helpers/pageElements.ts +27 -0
  117. package/src/helpers/request.ts +62 -0
  118. package/src/helpers/response.ts +411 -0
  119. package/src/helpers/str.ts +208 -0
  120. package/src/http/Http.ts +298 -0
  121. package/src/http/HttpClient.ts +289 -0
  122. package/src/http/Resource.ts +171 -0
  123. package/src/http/UploadedFile.ts +204 -0
  124. package/src/http/Uri.ts +490 -0
  125. package/src/http/index.ts +46 -0
  126. package/src/http/negotiate.ts +213 -0
  127. package/src/http/originGuard.ts +76 -0
  128. package/src/http/sniffContentType.ts +105 -0
  129. package/src/http/url.ts +204 -0
  130. package/src/http/withHeaders.ts +24 -0
  131. package/src/index.ts +250 -0
  132. package/src/lock/LockManager.ts +228 -0
  133. package/src/lock/config.ts +49 -0
  134. package/src/lock/drivers/LockDriver.ts +32 -0
  135. package/src/lock/drivers/MemoryLockDriver.ts +52 -0
  136. package/src/lock/drivers/RedisLockDriver.ts +58 -0
  137. package/src/lock/drivers/SqliteLockDriver.ts +85 -0
  138. package/src/lock/errors.ts +20 -0
  139. package/src/lock/facades/Lock.ts +114 -0
  140. package/src/lock/index.ts +53 -0
  141. package/src/logger/Log.ts +35 -0
  142. package/src/logger/LogManager.ts +430 -0
  143. package/src/logger/LoggerMiddleware.ts +125 -0
  144. package/src/logger/channels/ConsoleChannel.ts +139 -0
  145. package/src/logger/channels/DailyChannel.ts +74 -0
  146. package/src/logger/channels/NullChannel.ts +17 -0
  147. package/src/logger/channels/SingleChannel.ts +34 -0
  148. package/src/logger/channels/StackChannel.ts +29 -0
  149. package/src/logger/config.ts +90 -0
  150. package/src/logger/format.ts +96 -0
  151. package/src/logger/frameworkLog.ts +93 -0
  152. package/src/logger/index.ts +68 -0
  153. package/src/logger/renderTable.ts +111 -0
  154. package/src/logger/types.ts +212 -0
  155. package/src/macros/config.macro.ts +50 -0
  156. package/src/metrics/HttpMetrics.ts +114 -0
  157. package/src/metrics/index.ts +18 -0
  158. package/src/middleware/BaseMiddleware.ts +72 -0
  159. package/src/middleware/CorsMiddleware.ts +152 -0
  160. package/src/middleware/RateLimiter.ts +255 -0
  161. package/src/middleware/SecureHeadersMiddleware.ts +127 -0
  162. package/src/middleware/ThrottleMiddleware.ts +252 -0
  163. package/src/middleware/WebhookMiddleware.ts +204 -0
  164. package/src/pipeline/ContextRegistry.ts +42 -0
  165. package/src/pipeline/HttpContext.ts +865 -0
  166. package/src/pipeline/Pipeline.ts +150 -0
  167. package/src/pipeline/currentPage.ts +46 -0
  168. package/src/pipeline/types.ts +80 -0
  169. package/src/provider/LockProvider.ts +64 -0
  170. package/src/provider/LogProvider.ts +137 -0
  171. package/src/provider/ServiceProvider.ts +84 -0
  172. package/src/provider/StorageProvider.ts +45 -0
  173. package/src/router/FileRouter.ts +526 -0
  174. package/src/router/Route.ts +76 -0
  175. package/src/router/RouteHandler.ts +335 -0
  176. package/src/router/Router.ts +1247 -0
  177. package/src/router/domain.ts +65 -0
  178. package/src/security/index.ts +22 -0
  179. package/src/storage/FakeDisk.ts +233 -0
  180. package/src/storage/StorageFilesMiddleware.ts +150 -0
  181. package/src/storage/StorageManager.ts +173 -0
  182. package/src/storage/config.ts +47 -0
  183. package/src/storage/drivers/LocalDriver.ts +138 -0
  184. package/src/storage/drivers/S3Driver.ts +169 -0
  185. package/src/storage/errors.ts +135 -0
  186. package/src/storage/facades/Storage.ts +3 -0
  187. package/src/storage/global.d.ts +7 -0
  188. package/src/storage/index.ts +22 -0
  189. package/src/storage/root.ts +59 -0
  190. package/src/storage/types.ts +104 -0
  191. package/src/support/appKey.ts +38 -0
  192. package/src/support/cookie.ts +72 -0
  193. package/src/support/crypto.ts +52 -0
  194. package/src/support/deepMerge.ts +117 -0
  195. package/src/support/env.ts +71 -0
  196. package/src/support/network.ts +79 -0
  197. package/src/support/port.ts +197 -0
  198. package/src/support/str.ts +122 -0
  199. package/src/view/FileRouteResolver.ts +59 -0
  200. package/src/view/index.ts +144 -0
  201. package/src/view/jsx-runtime.ts +233 -0
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Health checks and their configuration (the `@zerotal/core/health` subpath).
3
+ * {@link Health} is a registry of named probes behind the framework's `/health`
4
+ * endpoint; `critical` checks drive overall readiness and the HTTP 503 response,
5
+ * while non-critical failures degrade the report without failing readiness.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { Health } from "@zerotal/core/health";
10
+ *
11
+ * Health.register("database", async () => {
12
+ * await DB.raw("select 1");
13
+ * }, { critical: true });
14
+ * ```
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+ export { Health } from "./Health.ts";
19
+ export type {
20
+ HealthStatus,
21
+ HealthResult,
22
+ HealthCheckFn,
23
+ HealthReport,
24
+ HealthCheckReport,
25
+ HealthConfigShape,
26
+ ResolvedHealthConfig,
27
+ } from "./Health.ts";
@@ -0,0 +1,435 @@
1
+ /**
2
+ * Fluent wrapper around an array. Chain transformations
3
+ * ({@link Collection.map | map}, {@link Collection.filter | filter},
4
+ * {@link Collection.where | where}, {@link Collection.pluck | pluck}, …), read
5
+ * aggregates ({@link Collection.sum | sum}, {@link Collection.first | first}, …),
6
+ * and unwrap with {@link Collection.toArray | toArray}. Most methods return a NEW
7
+ * `Collection`, leaving the original untouched, so chains read top-to-bottom.
8
+ *
9
+ * Create one with the {@link collect} helper. Extend all instances at once with
10
+ * the static {@link Collection.macro | macro}.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const users = collect([
15
+ * { name: 'Alice', team: 'a', score: 90 },
16
+ * { name: 'Bob', team: 'b', score: 70 },
17
+ * { name: 'Cara', team: 'a', score: 85 },
18
+ * ]);
19
+ *
20
+ * users
21
+ * .where('score', '>=', 80) // keep Alice and Cara
22
+ * .map((u) => ({ ...u, name: u.name.toUpperCase() }))
23
+ * .pluck('name') // Collection<string>
24
+ * .toArray(); // ['ALICE', 'CARA']
25
+ * ```
26
+ */
27
+ export class Collection<T> {
28
+ constructor(private _items: T[]) {}
29
+
30
+ // ── Transformation ────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Map every item through `fn`, returning a new Collection of the results.
34
+ * @category Transformation
35
+ */
36
+ map<U>(fn: (item: T, index: number) => U): Collection<U> {
37
+ return new Collection(this._items.map(fn));
38
+ }
39
+
40
+ /**
41
+ * Map each item to an array and flatten the results one level.
42
+ * @category Transformation
43
+ */
44
+ flatMap<U>(fn: (item: T, index: number) => U[]): Collection<U> {
45
+ return new Collection(this._items.flatMap(fn));
46
+ }
47
+
48
+ /**
49
+ * Keep only items for which `fn` returns truthy.
50
+ * @category Transformation
51
+ */
52
+ filter(fn: (item: T, index: number) => boolean): Collection<T> {
53
+ return new Collection(this._items.filter(fn));
54
+ }
55
+
56
+ /**
57
+ * Inverse of {@link Collection.filter} — drop items for which `fn` returns truthy.
58
+ * @category Transformation
59
+ */
60
+ reject(fn: (item: T, index: number) => boolean): Collection<T> {
61
+ return new Collection(this._items.filter((item, i) => !fn(item, i)));
62
+ }
63
+
64
+ /**
65
+ * Filter by a key/value pair (optionally with operator). With two arguments
66
+ * the operator defaults to `=`; supported operators are `=`/`==`, `!=`/`<>`,
67
+ * `>`, `>=`, `<`, `<=`.
68
+ *
69
+ * @category Transformation
70
+ * @example
71
+ * collect(users).where('active', true)
72
+ * collect(orders).where('total', '>', 100)
73
+ */
74
+ where(key: keyof T, operatorOrValue: unknown, value?: unknown): Collection<T> {
75
+ let operator: string;
76
+ let comparand: unknown;
77
+ if (value === undefined) {
78
+ operator = "=";
79
+ comparand = operatorOrValue;
80
+ } else {
81
+ operator = String(operatorOrValue);
82
+ comparand = value;
83
+ }
84
+
85
+ return this.filter((item) => {
86
+ const actual = (item as Record<string, unknown>)[String(key)];
87
+ switch (operator) {
88
+ case "=":
89
+ case "==":
90
+ return actual === comparand;
91
+ case "!=":
92
+ case "<>":
93
+ return actual !== comparand;
94
+ case ">":
95
+ return (actual as number) > (comparand as number);
96
+ case ">=":
97
+ return (actual as number) >= (comparand as number);
98
+ case "<":
99
+ return (actual as number) < (comparand as number);
100
+ case "<=":
101
+ return (actual as number) <= (comparand as number);
102
+ default:
103
+ return actual === comparand;
104
+ }
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Filter to items whose key value is in the list.
110
+ * @category Transformation
111
+ */
112
+ whereIn<K extends keyof T>(key: K, values: T[K][]): Collection<T> {
113
+ return this.filter((item) => values.includes(item[key]));
114
+ }
115
+
116
+ /**
117
+ * Filter to items whose key value is NOT in the list.
118
+ * @category Transformation
119
+ */
120
+ whereNotIn<K extends keyof T>(key: K, values: T[K][]): Collection<T> {
121
+ return this.filter((item) => !values.includes(item[key]));
122
+ }
123
+
124
+ /**
125
+ * Extract one key from each item into a new Collection of those values.
126
+ * @category Transformation
127
+ */
128
+ pluck<K extends keyof T>(key: K): Collection<T[K]> {
129
+ return new Collection(this._items.map((item) => item[key]));
130
+ }
131
+
132
+ /**
133
+ * Group items into a plain object keyed by a property or callback result.
134
+ * @category Transformation
135
+ */
136
+ groupBy<K extends string>(keyOrFn: keyof T | ((item: T) => K)): Record<K, T[]> {
137
+ const fn: (item: T) => K =
138
+ typeof keyOrFn === "function"
139
+ ? (keyOrFn as (item: T) => K)
140
+ : (item) => String((item as Record<string, unknown>)[String(keyOrFn)]) as K;
141
+
142
+ const result = {} as Record<K, T[]>;
143
+ for (const item of this._items) {
144
+ const groupKey = fn(item);
145
+ if (!result[groupKey]) result[groupKey] = [];
146
+ result[groupKey].push(item);
147
+ }
148
+ return result;
149
+ }
150
+
151
+ /**
152
+ * Index items into a plain object keyed by a property — last duplicate wins.
153
+ * @category Transformation
154
+ */
155
+ keyBy<K extends keyof T>(key: K): Record<string, T> {
156
+ const result: Record<string, T> = {};
157
+ for (const item of this._items) {
158
+ result[String(item[key])] = item;
159
+ }
160
+ return result;
161
+ }
162
+
163
+ /**
164
+ * Return a new Collection sorted by a key, ascending by default.
165
+ * @category Transformation
166
+ */
167
+ sortBy<K extends keyof T>(key: K, direction: "asc" | "desc" = "asc"): Collection<T> {
168
+ const sorted = [...this._items].sort((a, b) => {
169
+ const valueA = a[key] as unknown;
170
+ const valueB = b[key] as unknown;
171
+ if (valueA === valueB) return 0;
172
+ const comparison = (valueA as number) < (valueB as number) ? -1 : 1;
173
+ return direction === "asc" ? comparison : -comparison;
174
+ });
175
+ return new Collection(sorted);
176
+ }
177
+
178
+ /**
179
+ * Remove duplicate values (for primitive collections) or by key.
180
+ * @category Transformation
181
+ */
182
+ unique(key?: keyof T): Collection<T> {
183
+ if (!key) {
184
+ return new Collection([...new Set(this._items)]);
185
+ }
186
+ const seen = new Set<unknown>();
187
+ return this.filter((item) => {
188
+ const keyValue = item[key];
189
+ if (seen.has(keyValue)) return false;
190
+ seen.add(keyValue);
191
+ return true;
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Split into a Collection of arrays, each at most `size` items long.
197
+ * @category Transformation
198
+ */
199
+ chunk(size: number): Collection<T[]> {
200
+ const result: T[][] = [];
201
+ for (let start = 0; start < this._items.length; start += size) {
202
+ result.push(this._items.slice(start, start + size));
203
+ }
204
+ return new Collection(result);
205
+ }
206
+
207
+ /**
208
+ * Flatten a Collection of arrays one level deep.
209
+ * @category Transformation
210
+ */
211
+ flatten(): Collection<T extends unknown[] ? T[number] : T> {
212
+ return new Collection(
213
+ (this._items as unknown[]).flat() as (T extends unknown[] ? T[number] : T)[],
214
+ );
215
+ }
216
+
217
+ /**
218
+ * Return a new Collection with the items in reverse order.
219
+ * @category Transformation
220
+ */
221
+ reverse(): Collection<T> {
222
+ return new Collection([...this._items].reverse());
223
+ }
224
+
225
+ /**
226
+ * Take the first `count` items as a new Collection, or — when called with no
227
+ * argument — return the single first item (or undefined).
228
+ * @category Transformation
229
+ */
230
+ take<N extends number | undefined = undefined>(
231
+ count?: N,
232
+ ): N extends undefined ? T | undefined : Collection<T> {
233
+ if (count === undefined) return this._items[0] as never;
234
+ return new Collection(this._items.slice(0, count)) as never;
235
+ }
236
+
237
+ /**
238
+ * Skip the first `count` items, returning the rest as a new Collection.
239
+ * @category Transformation
240
+ */
241
+ skip(count: number): Collection<T> {
242
+ return new Collection(this._items.slice(count));
243
+ }
244
+
245
+ /**
246
+ * Run `fn` for each item (for side effects) and return the same Collection.
247
+ * @category Access
248
+ */
249
+ each(fn: (item: T, index: number) => void): this {
250
+ this._items.forEach(fn);
251
+ return this;
252
+ }
253
+
254
+ // ── Aggregates ────────────────────────────────────────────────────────────
255
+
256
+ /**
257
+ * The number of items.
258
+ * @category Aggregation
259
+ */
260
+ count(): number {
261
+ return this._items.length;
262
+ }
263
+
264
+ /**
265
+ * Whether the collection has no items.
266
+ * @category Aggregation
267
+ */
268
+ isEmpty(): boolean {
269
+ return this._items.length === 0;
270
+ }
271
+ /**
272
+ * Whether the collection has at least one item.
273
+ * @category Aggregation
274
+ */
275
+ isNotEmpty(): boolean {
276
+ return this._items.length > 0;
277
+ }
278
+
279
+ /**
280
+ * The first item (optionally the first matching `fn`), or undefined.
281
+ * @category Access
282
+ */
283
+ first(fn?: (item: T) => boolean): T | undefined {
284
+ return fn ? this._items.find(fn) : this._items[0];
285
+ }
286
+
287
+ /**
288
+ * The last item (optionally the last matching `fn`), or undefined.
289
+ * @category Access
290
+ */
291
+ last(fn?: (item: T) => boolean): T | undefined {
292
+ if (fn) {
293
+ const matches = this._items.filter(fn);
294
+ return matches[matches.length - 1];
295
+ }
296
+ return this._items[this._items.length - 1];
297
+ }
298
+
299
+ /**
300
+ * Sum the items, or the numeric value at `key` on each item.
301
+ * @category Aggregation
302
+ */
303
+ sum(key?: keyof T): number {
304
+ if (key) {
305
+ return this._items.reduce((acc, item) => acc + Number(item[key]), 0);
306
+ }
307
+ return (this._items as unknown as number[]).reduce((a, b) => a + b, 0);
308
+ }
309
+
310
+ /**
311
+ * The mean of the items (or of `key` on each item); `0` when empty.
312
+ * @category Aggregation
313
+ */
314
+ avg(key?: keyof T): number {
315
+ if (this._items.length === 0) return 0;
316
+ return this.sum(key) / this._items.length;
317
+ }
318
+
319
+ /**
320
+ * The smallest value (or smallest `key` value); `0` when empty.
321
+ * @category Aggregation
322
+ */
323
+ min(key?: keyof T): number {
324
+ if (this._items.length === 0) return 0;
325
+ if (key) return Math.min(...this._items.map((item) => Number(item[key])));
326
+ return Math.min(...(this._items as unknown as number[]));
327
+ }
328
+
329
+ /**
330
+ * The largest value (or largest `key` value); `0` when empty.
331
+ * @category Aggregation
332
+ */
333
+ max(key?: keyof T): number {
334
+ if (this._items.length === 0) return 0;
335
+ if (key) return Math.max(...this._items.map((item) => Number(item[key])));
336
+ return Math.max(...(this._items as unknown as number[]));
337
+ }
338
+
339
+ /**
340
+ * Whether the collection contains a value, or any item matching a predicate.
341
+ * @category Access
342
+ */
343
+ contains(valueOrFn: T | ((item: T) => boolean)): boolean {
344
+ if (typeof valueOrFn === "function") {
345
+ return this._items.some(valueOrFn as (item: T) => boolean);
346
+ }
347
+ return this._items.includes(valueOrFn);
348
+ }
349
+
350
+ // ── Output ────────────────────────────────────────────────────────────────
351
+
352
+ /**
353
+ * A shallow copy of the underlying items as a plain array.
354
+ * @category Conversion
355
+ */
356
+ toArray(): T[] {
357
+ return [...this._items];
358
+ }
359
+
360
+ /**
361
+ * The items serialized to a JSON string.
362
+ * @category Conversion
363
+ */
364
+ toJson(): string {
365
+ return JSON.stringify(this._items);
366
+ }
367
+
368
+ /**
369
+ * A shallow copy of the items (alias of {@link Collection.toArray}).
370
+ * @category Conversion
371
+ */
372
+ values(): T[] {
373
+ return [...this._items];
374
+ }
375
+
376
+ /**
377
+ * The number of items (property form of {@link Collection.count}).
378
+ * @category Aggregation
379
+ */
380
+ get length(): number {
381
+ return this._items.length;
382
+ }
383
+
384
+ /**
385
+ * Iterate the items directly, e.g. in a `for...of` loop or spread.
386
+ * @category Conversion
387
+ */
388
+ [Symbol.iterator](): Iterator<T> {
389
+ return this._items[Symbol.iterator]();
390
+ }
391
+
392
+ // ── Macros ────────────────────────────────────────────────────────────────
393
+
394
+ /**
395
+ * Register a macro (custom method) on all Collection instances.
396
+ *
397
+ * The function is added to `Collection.prototype`, so it is available on
398
+ * every `collect()` result immediately. Use `this` inside the function to
399
+ * access the collection's own methods (`this.filter()`, `this.map()`, etc.).
400
+ *
401
+ * Add a module augmentation to your project for full TypeScript type safety:
402
+ *
403
+ * @example
404
+ * // In AppServiceProvider.onBooted():
405
+ * Collection.macro('filterActive', function(this: Collection<{ active: boolean }>) {
406
+ * return this.filter((item) => item.active);
407
+ * });
408
+ *
409
+ * // In types.d.ts (for type safety):
410
+ * declare module '@zerotal/core' {
411
+ * interface Collection<T> {
412
+ * filterActive(): Collection<T & { active: boolean }>;
413
+ * }
414
+ * }
415
+ *
416
+ * // In a controller:
417
+ * const active = collect(users).filterActive();
418
+ *
419
+ * @category Mutation
420
+ */
421
+ static macro(name: string, fn: (this: Collection<unknown>, ...args: unknown[]) => unknown): void {
422
+ (Collection.prototype as unknown as Record<string, unknown>)[name] = fn;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Create a Collection from an array.
428
+ *
429
+ * @example
430
+ * const users = collect([{ name: 'Alice', score: 90 }, { name: 'Bob', score: 70 }]);
431
+ * users.where('score', '>', 80).pluck('name').first(); // 'Alice'
432
+ */
433
+ export function collect<T>(items: T[]): Collection<T> {
434
+ return new Collection(items);
435
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The global `config()` helper — a typed accessor over the application's
3
+ * `ConfigManager`, with `.set`, `.require`, `.all`, and `.safe` attached for
4
+ * reading and writing configuration from anywhere in a booted app.
5
+ */
6
+ import { currentApp } from "../application/currentApp.ts";
7
+ import type { ConfigManager } from "../config/ConfigManager.ts";
8
+ import type { ConfigPath, ConfigValue } from "../config/registry.ts";
9
+
10
+ function _manager(): ConfigManager {
11
+ return currentApp().container.makeSync("config") as ConfigManager;
12
+ }
13
+
14
+ /**
15
+ * Read a configuration value by dot-path, with an optional fallback.
16
+ *
17
+ * Known paths (those declared on the augmented `ConfigRegistry`) are resolved by
18
+ * the typed overloads first, so `config('app.name')` is inferred as `string`; a
19
+ * plain `string` path falls through to the untyped escape-hatch overloads.
20
+ */
21
+ function config<P extends ConfigPath>(path: P): ConfigValue<P>;
22
+ function config<P extends ConfigPath>(path: P, fallback: ConfigValue<P>): ConfigValue<P>;
23
+ function config(path: string): unknown;
24
+ function config<T>(path: string, fallback: T): T;
25
+ function config(path: string, fallback?: unknown): unknown {
26
+ return _manager().get(path, fallback);
27
+ }
28
+
29
+ // Typed setter — value is checked against the path's declared shape.
30
+ function _set<P extends ConfigPath>(path: P, value: ConfigValue<P>): void;
31
+ function _set(path: string, value: unknown): void;
32
+ function _set(path: string, value: unknown): void {
33
+ _manager().set(path, value);
34
+ }
35
+ config.set = _set;
36
+
37
+ // Typed require — throws when the path is absent.
38
+ function _require<P extends ConfigPath>(path: P): ConfigValue<P>;
39
+ function _require<T = unknown>(path: string): T;
40
+ function _require(path: string): unknown {
41
+ return _manager().require(path);
42
+ }
43
+ config.require = _require;
44
+
45
+ config.all = (): Record<string, unknown> => _manager().all();
46
+
47
+ /** Read config without throwing when no app is booted (returns the fallback). */
48
+ function _safe<P extends ConfigPath>(path: P, fallback: ConfigValue<P>): ConfigValue<P>;
49
+ function _safe<T>(path: string, fallback: T): T;
50
+ function _safe(path: string, fallback: unknown): unknown {
51
+ try {
52
+ return _manager().get(path, fallback);
53
+ } catch {
54
+ return fallback;
55
+ }
56
+ }
57
+ config.safe = _safe;
58
+
59
+ export { config };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Chainable value wrapper for building readable sync transformation pipelines.
3
+ *
4
+ * Use `fluent(value)` when you have a series of sync steps that would otherwise
5
+ * require intermediate `let` variables. For async steps, use the standalone
6
+ * `pipeAsync` / `tapAsync` helpers.
7
+ *
8
+ * @example
9
+ * const hash = fluent(rawEmail)
10
+ * .pipe((e) => e.trim().toLowerCase())
11
+ * .tap((e) => logger.info(`Registering: ${e}`))
12
+ * .pipe((e) => crypto.createHash('sha256').update(e).digest('hex'))
13
+ * .get();
14
+ */
15
+ export class Fluent<T> {
16
+ readonly #value: T;
17
+
18
+ constructor(value: T) {
19
+ this.#value = value;
20
+ }
21
+
22
+ /** Transform the wrapped value and return a new Fluent. */
23
+ pipe<R>(fn: (val: T) => R): Fluent<R> {
24
+ return new Fluent(fn(this.#value));
25
+ }
26
+
27
+ /** Run a side-effect and return the same Fluent unchanged. */
28
+ tap(fn: (val: T) => void): this {
29
+ fn(this.#value);
30
+ return this;
31
+ }
32
+
33
+ /** Unwrap the final value. */
34
+ get(): T {
35
+ return this.#value;
36
+ }
37
+
38
+ /** Template-literal / string-coercion support. */
39
+ toString(): string {
40
+ return String(this.#value);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Wrap a value in a {@link Fluent} builder to start a chainable pipeline.
46
+ *
47
+ * @example
48
+ * fluent(user.name).pipe(Str.slugify).get() // 'alice-smith'
49
+ */
50
+ export function fluent<T>(value: T): Fluent<T> {
51
+ return new Fluent(value);
52
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * HTML escaping for server-rendered output — the framework's one escaper.
3
+ *
4
+ * Delegates to `Bun.escapeHTML`, which is SIMD-optimized and escapes the full
5
+ * set (`&` `<` `>` `"` `'`, emitting `&#x27;` for the apostrophe). Safe for
6
+ * both text content and double-quoted attribute values; both JSX runtimes and
7
+ * the docs app render through it, so it runs on every SSR text child.
8
+ */
9
+ export function escapeHtml(value: string): string {
10
+ return Bun.escapeHTML(value);
11
+ }