@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.
- package/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/package.json +72 -0
- package/src/application/Application.ts +1671 -0
- package/src/application/BootDoctor.ts +108 -0
- package/src/application/DevErrorPage.ts +567 -0
- package/src/application/ExceptionHandler.ts +183 -0
- package/src/application/currentApp.ts +73 -0
- package/src/assets/assets.ts +79 -0
- package/src/assets/index.ts +16 -0
- package/src/auth/AuthenticatedUser.ts +18 -0
- package/src/build/PackageLinter.ts +146 -0
- package/src/build/PackageScaffold.ts +127 -0
- package/src/build/codemod.ts +64 -0
- package/src/build/index.ts +12 -0
- package/src/command/Command.ts +254 -0
- package/src/command/CommandRunner.ts +593 -0
- package/src/command/OutputWriter.ts +61 -0
- package/src/command/builtin/CompileCommand.ts +46 -0
- package/src/command/builtin/CssBuildCommand.ts +71 -0
- package/src/command/builtin/KeyGenerateCommand.ts +58 -0
- package/src/command/builtin/LintPackagesCommand.ts +72 -0
- package/src/command/builtin/MakeCommandCommand.ts +85 -0
- package/src/command/builtin/MakeControllerCommand.ts +95 -0
- package/src/command/builtin/MakeEventCommand.ts +85 -0
- package/src/command/builtin/MakeJobCommand.ts +53 -0
- package/src/command/builtin/MakeListenerCommand.ts +35 -0
- package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
- package/src/command/builtin/MakeNotificationCommand.ts +48 -0
- package/src/command/builtin/MakeObserverCommand.ts +78 -0
- package/src/command/builtin/MakePackageCommand.ts +45 -0
- package/src/command/builtin/MakePolicyCommand.ts +66 -0
- package/src/command/builtin/MakeProviderCommand.ts +75 -0
- package/src/command/builtin/MakeRequestCommand.ts +47 -0
- package/src/command/builtin/MakeResourceCommand.ts +61 -0
- package/src/command/builtin/MakeTestCommand.ts +120 -0
- package/src/command/builtin/ReloadCommand.ts +52 -0
- package/src/command/builtin/ReplCommand.ts +174 -0
- package/src/command/builtin/RouteListCommand.ts +188 -0
- package/src/command/builtin/ServeCommand.ts +321 -0
- package/src/command/builtin/StartCommand.ts +3 -0
- package/src/command/builtin/StatusCommand.ts +71 -0
- package/src/command/builtin/TestCommand.ts +172 -0
- package/src/command/builtin/WorkerCommand.ts +27 -0
- package/src/command/builtin/index.ts +53 -0
- package/src/command/scaffold/worker.ts.txt +12 -0
- package/src/command/scaffold/zerotal.ts.txt +26 -0
- package/src/command/startZerotal.ts +55 -0
- package/src/config/AppConfig.ts +253 -0
- package/src/config/ConfigLoader.ts +117 -0
- package/src/config/ConfigManager.ts +169 -0
- package/src/config/index.ts +46 -0
- package/src/config/registry.ts +59 -0
- package/src/config/validation.ts +117 -0
- package/src/container/Container.ts +606 -0
- package/src/container/ContextualBindingBuilder.ts +57 -0
- package/src/container/ScopedResolver.ts +117 -0
- package/src/container/index.ts +32 -0
- package/src/container/inject.ts +55 -0
- package/src/container/types.ts +71 -0
- package/src/context/RequestContext.ts +91 -0
- package/src/contracts/auth.ts +24 -0
- package/src/contracts/index.ts +23 -0
- package/src/contracts/session.ts +70 -0
- package/src/contracts/transaction.ts +26 -0
- package/src/conventions/ConventionLoader.ts +128 -0
- package/src/conventions/builtinConcerns.ts +131 -0
- package/src/crypt/Crypt.ts +141 -0
- package/src/crypt/URLSigner.ts +96 -0
- package/src/datetime/Carbon.ts +1396 -0
- package/src/datetime/CarbonInterval.ts +421 -0
- package/src/datetime/clock.ts +28 -0
- package/src/datetime/index.ts +23 -0
- package/src/datetime/temporal-shim.ts +1 -0
- package/src/dev/BuildOutput.ts +131 -0
- package/src/dev/CssPlugins.ts +184 -0
- package/src/dev/DevBuildHook.ts +74 -0
- package/src/dev/DevOrchestrator.ts +213 -0
- package/src/dev/DevReloadMiddleware.ts +101 -0
- package/src/dev/DevReloadServer.ts +85 -0
- package/src/dev/DevWsServer.ts +45 -0
- package/src/dev/index.ts +19 -0
- package/src/dev/reloadClient.ts +39 -0
- package/src/env/Def.ts +232 -0
- package/src/env/EnvSchema.ts +105 -0
- package/src/env/index.ts +34 -0
- package/src/env/t.ts +128 -0
- package/src/errors/ConfigError.ts +12 -0
- package/src/errors/ContainerErrors.ts +143 -0
- package/src/errors/HttpError.ts +127 -0
- package/src/errors/ValidationError.ts +19 -0
- package/src/errors/ZerotalError.ts +25 -0
- package/src/errors/index.ts +46 -0
- package/src/events/CallQueuedListener.ts +66 -0
- package/src/events/Emitter.ts +280 -0
- package/src/events/EventFake.ts +160 -0
- package/src/events/FrameworkEvents.ts +252 -0
- package/src/facade/Facade.ts +101 -0
- package/src/facade/facades/App.ts +155 -0
- package/src/facade/facades/Artisan.ts +63 -0
- package/src/facade/facades/Config.ts +21 -0
- package/src/facade/facades/Events.ts +19 -0
- package/src/facade/facades/index.ts +28 -0
- package/src/global.d.ts +9 -0
- package/src/hash/Hash.ts +60 -0
- package/src/health/Health.ts +221 -0
- package/src/health/index.ts +27 -0
- package/src/helpers/Collection.ts +435 -0
- package/src/helpers/config.ts +59 -0
- package/src/helpers/fluent.ts +52 -0
- package/src/helpers/html.ts +11 -0
- package/src/helpers/index.ts +266 -0
- package/src/helpers/make.ts +35 -0
- package/src/helpers/markdown.ts +73 -0
- package/src/helpers/pageElements.ts +27 -0
- package/src/helpers/request.ts +62 -0
- package/src/helpers/response.ts +411 -0
- package/src/helpers/str.ts +208 -0
- package/src/http/Http.ts +298 -0
- package/src/http/HttpClient.ts +289 -0
- package/src/http/Resource.ts +171 -0
- package/src/http/UploadedFile.ts +204 -0
- package/src/http/Uri.ts +490 -0
- package/src/http/index.ts +46 -0
- package/src/http/negotiate.ts +213 -0
- package/src/http/originGuard.ts +76 -0
- package/src/http/sniffContentType.ts +105 -0
- package/src/http/url.ts +204 -0
- package/src/http/withHeaders.ts +24 -0
- package/src/index.ts +250 -0
- package/src/lock/LockManager.ts +228 -0
- package/src/lock/config.ts +49 -0
- package/src/lock/drivers/LockDriver.ts +32 -0
- package/src/lock/drivers/MemoryLockDriver.ts +52 -0
- package/src/lock/drivers/RedisLockDriver.ts +58 -0
- package/src/lock/drivers/SqliteLockDriver.ts +85 -0
- package/src/lock/errors.ts +20 -0
- package/src/lock/facades/Lock.ts +114 -0
- package/src/lock/index.ts +53 -0
- package/src/logger/Log.ts +35 -0
- package/src/logger/LogManager.ts +430 -0
- package/src/logger/LoggerMiddleware.ts +125 -0
- package/src/logger/channels/ConsoleChannel.ts +139 -0
- package/src/logger/channels/DailyChannel.ts +74 -0
- package/src/logger/channels/NullChannel.ts +17 -0
- package/src/logger/channels/SingleChannel.ts +34 -0
- package/src/logger/channels/StackChannel.ts +29 -0
- package/src/logger/config.ts +90 -0
- package/src/logger/format.ts +96 -0
- package/src/logger/frameworkLog.ts +93 -0
- package/src/logger/index.ts +68 -0
- package/src/logger/renderTable.ts +111 -0
- package/src/logger/types.ts +212 -0
- package/src/macros/config.macro.ts +50 -0
- package/src/metrics/HttpMetrics.ts +114 -0
- package/src/metrics/index.ts +18 -0
- package/src/middleware/BaseMiddleware.ts +72 -0
- package/src/middleware/CorsMiddleware.ts +152 -0
- package/src/middleware/RateLimiter.ts +255 -0
- package/src/middleware/SecureHeadersMiddleware.ts +127 -0
- package/src/middleware/ThrottleMiddleware.ts +252 -0
- package/src/middleware/WebhookMiddleware.ts +204 -0
- package/src/pipeline/ContextRegistry.ts +42 -0
- package/src/pipeline/HttpContext.ts +865 -0
- package/src/pipeline/Pipeline.ts +150 -0
- package/src/pipeline/currentPage.ts +46 -0
- package/src/pipeline/types.ts +80 -0
- package/src/provider/LockProvider.ts +64 -0
- package/src/provider/LogProvider.ts +137 -0
- package/src/provider/ServiceProvider.ts +84 -0
- package/src/provider/StorageProvider.ts +45 -0
- package/src/router/FileRouter.ts +526 -0
- package/src/router/Route.ts +76 -0
- package/src/router/RouteHandler.ts +335 -0
- package/src/router/Router.ts +1247 -0
- package/src/router/domain.ts +65 -0
- package/src/security/index.ts +22 -0
- package/src/storage/FakeDisk.ts +233 -0
- package/src/storage/StorageFilesMiddleware.ts +150 -0
- package/src/storage/StorageManager.ts +173 -0
- package/src/storage/config.ts +47 -0
- package/src/storage/drivers/LocalDriver.ts +138 -0
- package/src/storage/drivers/S3Driver.ts +169 -0
- package/src/storage/errors.ts +135 -0
- package/src/storage/facades/Storage.ts +3 -0
- package/src/storage/global.d.ts +7 -0
- package/src/storage/index.ts +22 -0
- package/src/storage/root.ts +59 -0
- package/src/storage/types.ts +104 -0
- package/src/support/appKey.ts +38 -0
- package/src/support/cookie.ts +72 -0
- package/src/support/crypto.ts +52 -0
- package/src/support/deepMerge.ts +117 -0
- package/src/support/env.ts +71 -0
- package/src/support/network.ts +79 -0
- package/src/support/port.ts +197 -0
- package/src/support/str.ts +122 -0
- package/src/view/FileRouteResolver.ts +59 -0
- package/src/view/index.ts +144 -0
- 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 `'` 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
|
+
}
|