alepha 0.14.4 → 0.15.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/README.md +1 -4
- package/dist/api/audits/index.d.ts +619 -731
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/files/index.d.ts +185 -298
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +0 -1
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +245 -356
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/notifications/index.d.ts +238 -350
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/parameters/index.d.ts +499 -611
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/users/index.browser.js +1 -2
- package/dist/api/users/index.browser.js.map +1 -1
- package/dist/api/users/index.d.ts +1697 -1804
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +178 -151
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +132 -132
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/batch/index.d.ts +122 -122
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +1 -2
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +163 -163
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/cache/core/index.d.ts +46 -46
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cli/index.d.ts +302 -299
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +966 -564
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +303 -299
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +11 -7
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +419 -99
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +718 -625
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +420 -99
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +419 -99
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.d.ts +44 -44
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/datetime/index.js +4 -4
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/index.d.ts +97 -50
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +129 -33
- package/dist/email/index.js.map +1 -1
- package/dist/fake/index.d.ts +7981 -14
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/file/index.d.ts +523 -390
- package/dist/file/index.d.ts.map +1 -1
- package/dist/file/index.js +253 -1
- package/dist/file/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +208 -208
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/redis/index.d.ts.map +1 -1
- package/dist/logger/index.d.ts +25 -26
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +197 -197
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/orm/chunk-DtkW-qnP.js +38 -0
- package/dist/orm/index.browser.js.map +1 -1
- package/dist/orm/index.bun.js +2814 -0
- package/dist/orm/index.bun.js.map +1 -0
- package/dist/orm/index.d.ts +1205 -1057
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +2056 -1753
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +248 -248
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/redis/index.bun.js +285 -0
- package/dist/redis/index.bun.js.map +1 -0
- package/dist/redis/index.d.ts +118 -136
- package/dist/redis/index.d.ts.map +1 -1
- package/dist/redis/index.js +18 -38
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.d.ts +69 -69
- package/dist/retry/index.d.ts.map +1 -1
- package/dist/router/index.d.ts +6 -6
- package/dist/router/index.d.ts.map +1 -1
- package/dist/scheduler/index.d.ts +25 -25
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/security/index.browser.js +5 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +417 -254
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +386 -86
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +277 -277
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +20 -20
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cache/index.d.ts +60 -57
- package/dist/server/cache/index.d.ts.map +1 -1
- package/dist/server/cache/index.js +1 -1
- package/dist/server/cache/index.js.map +1 -1
- package/dist/server/compress/index.d.ts +3 -3
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/cookies/index.d.ts +6 -6
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/cookies/index.js +3 -3
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.d.ts +242 -150
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +288 -122
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +11 -12
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/health/index.d.ts +0 -1
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/helmet/index.d.ts +2 -2
- package/dist/server/helmet/index.d.ts.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.d.ts +84 -85
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js +1 -2
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/multipart/index.d.ts +6 -6
- package/dist/server/multipart/index.d.ts.map +1 -1
- package/dist/server/proxy/index.d.ts +102 -103
- package/dist/server/proxy/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.d.ts +16 -16
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/static/index.d.ts +44 -44
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/swagger/index.d.ts +48 -49
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +1 -2
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +13 -11
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +7 -7
- package/dist/sms/index.js.map +1 -1
- package/dist/thread/index.d.ts +71 -72
- package/dist/thread/index.d.ts.map +1 -1
- package/dist/topic/core/index.d.ts +318 -318
- package/dist/topic/core/index.d.ts.map +1 -1
- package/dist/topic/redis/index.d.ts +6 -6
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/dist/vite/index.d.ts +5720 -159
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +41 -18
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.browser.js +6 -6
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.d.ts +247 -247
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +6 -6
- package/dist/websocket/index.js.map +1 -1
- package/package.json +9 -14
- package/src/api/files/controllers/AdminFileStatsController.ts +0 -1
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +5 -0
- package/src/api/users/controllers/{UserRealmController.ts → RealmController.ts} +11 -11
- package/src/api/users/entities/users.ts +1 -1
- package/src/api/users/index.ts +8 -8
- package/src/api/users/primitives/{$userRealm.ts → $realm.ts} +17 -19
- package/src/api/users/providers/{UserRealmProvider.ts → RealmProvider.ts} +26 -30
- package/src/api/users/schemas/{userRealmConfigSchema.ts → realmConfigSchema.ts} +2 -2
- package/src/api/users/services/CredentialService.ts +7 -7
- package/src/api/users/services/IdentityService.ts +4 -4
- package/src/api/users/services/RegistrationService.spec.ts +25 -27
- package/src/api/users/services/RegistrationService.ts +38 -27
- package/src/api/users/services/SessionCrudService.ts +3 -3
- package/src/api/users/services/SessionService.spec.ts +3 -3
- package/src/api/users/services/SessionService.ts +28 -9
- package/src/api/users/services/UserService.ts +7 -7
- package/src/batch/providers/BatchProvider.ts +1 -2
- package/src/cli/apps/AlephaPackageBuilderCli.ts +38 -19
- package/src/cli/assets/apiHelloControllerTs.ts +18 -0
- package/src/cli/assets/apiIndexTs.ts +16 -0
- package/src/cli/assets/claudeMd.ts +303 -0
- package/src/cli/assets/mainBrowserTs.ts +2 -2
- package/src/cli/assets/mainServerTs.ts +24 -0
- package/src/cli/assets/webAppRouterTs.ts +15 -0
- package/src/cli/assets/webHelloComponentTsx.ts +16 -0
- package/src/cli/assets/webIndexTs.ts +16 -0
- package/src/cli/commands/build.ts +41 -21
- package/src/cli/commands/db.ts +21 -18
- package/src/cli/commands/deploy.ts +17 -5
- package/src/cli/commands/dev.ts +13 -17
- package/src/cli/commands/format.ts +8 -2
- package/src/cli/commands/init.ts +74 -29
- package/src/cli/commands/lint.ts +8 -2
- package/src/cli/commands/test.ts +8 -2
- package/src/cli/commands/typecheck.ts +5 -1
- package/src/cli/commands/verify.ts +4 -2
- package/src/cli/services/AlephaCliUtils.ts +39 -600
- package/src/cli/services/PackageManagerUtils.ts +301 -0
- package/src/cli/services/ProjectScaffolder.ts +306 -0
- package/src/command/helpers/Runner.ts +15 -3
- package/src/core/__tests__/Alepha-graph.spec.ts +4 -0
- package/src/core/index.shared.ts +1 -0
- package/src/core/index.ts +2 -0
- package/src/core/primitives/$hook.ts +6 -2
- package/src/core/primitives/$module.spec.ts +4 -0
- package/src/core/providers/AlsProvider.ts +1 -1
- package/src/core/providers/CodecManager.spec.ts +12 -6
- package/src/core/providers/CodecManager.ts +26 -6
- package/src/core/providers/EventManager.ts +169 -13
- package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +621 -0
- package/src/core/providers/KeylessJsonSchemaCodec.ts +407 -0
- package/src/core/providers/StateManager.spec.ts +27 -16
- package/src/email/providers/LocalEmailProvider.spec.ts +111 -87
- package/src/email/providers/LocalEmailProvider.ts +52 -15
- package/src/email/providers/NodemailerEmailProvider.ts +167 -56
- package/src/file/errors/FileError.ts +7 -0
- package/src/file/index.ts +9 -1
- package/src/file/providers/MemoryFileSystemProvider.ts +393 -0
- package/src/orm/index.browser.ts +1 -19
- package/src/orm/index.bun.ts +77 -0
- package/src/orm/index.shared-server.ts +22 -0
- package/src/orm/index.shared.ts +15 -0
- package/src/orm/index.ts +19 -39
- package/src/orm/providers/drivers/BunPostgresProvider.ts +3 -5
- package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -1
- package/src/orm/providers/drivers/CloudflareD1Provider.ts +4 -0
- package/src/orm/providers/drivers/DatabaseProvider.ts +4 -0
- package/src/orm/providers/drivers/PglitePostgresProvider.ts +4 -0
- package/src/orm/services/Repository.ts +8 -0
- package/src/redis/index.bun.ts +35 -0
- package/src/redis/providers/BunRedisProvider.ts +12 -43
- package/src/redis/providers/BunRedisSubscriberProvider.ts +2 -3
- package/src/redis/providers/NodeRedisProvider.ts +16 -34
- package/src/{server/security → security}/__tests__/BasicAuth.spec.ts +11 -11
- package/src/{server/security → security}/__tests__/ServerSecurityProvider-realm.spec.ts +21 -16
- package/src/{server/security/providers → security/__tests__}/ServerSecurityProvider.spec.ts +5 -5
- package/src/security/index.browser.ts +5 -0
- package/src/security/index.ts +90 -7
- package/src/security/primitives/{$realm.spec.ts → $issuer.spec.ts} +11 -11
- package/src/security/primitives/{$realm.ts → $issuer.ts} +20 -17
- package/src/security/primitives/$role.ts +5 -5
- package/src/security/primitives/$serviceAccount.spec.ts +5 -5
- package/src/security/primitives/$serviceAccount.ts +3 -3
- package/src/{server/security → security}/providers/ServerSecurityProvider.ts +5 -7
- package/src/server/auth/primitives/$auth.ts +10 -10
- package/src/server/auth/primitives/$authCredentials.ts +3 -3
- package/src/server/auth/primitives/$authGithub.ts +3 -3
- package/src/server/auth/primitives/$authGoogle.ts +3 -3
- package/src/server/auth/providers/ServerAuthProvider.ts +13 -13
- package/src/server/cache/providers/ServerCacheProvider.ts +1 -1
- package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -3
- package/src/server/core/providers/NodeHttpServerProvider.ts +25 -6
- package/src/server/core/providers/ServerBodyParserProvider.ts +19 -23
- package/src/server/core/providers/ServerLoggerProvider.ts +23 -19
- package/src/server/core/providers/ServerProvider.ts +144 -21
- package/src/server/core/providers/ServerRouterProvider.ts +259 -115
- package/src/server/core/providers/ServerTimingProvider.ts +2 -2
- package/src/server/links/index.ts +1 -1
- package/src/server/links/providers/LinkProvider.ts +1 -1
- package/src/server/swagger/index.ts +1 -1
- package/src/sms/providers/LocalSmsProvider.spec.ts +153 -111
- package/src/sms/providers/LocalSmsProvider.ts +8 -7
- package/src/vite/helpers/boot.ts +28 -17
- package/src/vite/tasks/buildServer.ts +12 -1
- package/src/vite/tasks/devServer.ts +3 -1
- package/src/vite/tasks/generateCloudflare.ts +7 -0
- package/dist/server/security/index.browser.js +0 -13
- package/dist/server/security/index.browser.js.map +0 -1
- package/dist/server/security/index.d.ts +0 -173
- package/dist/server/security/index.d.ts.map +0 -1
- package/dist/server/security/index.js +0 -311
- package/dist/server/security/index.js.map +0 -1
- package/src/cli/assets/appRouterTs.ts +0 -9
- package/src/cli/assets/mainTs.ts +0 -13
- package/src/server/security/index.browser.ts +0 -10
- package/src/server/security/index.ts +0 -94
- /package/src/{server/security → security}/primitives/$basicAuth.ts +0 -0
- /package/src/{server/security → security}/providers/ServerBasicAuthProvider.ts +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { Readable as NodeStream } from "node:stream";
|
|
2
3
|
import { ReadableStream as NodeWebStream } from "node:stream/web";
|
|
3
|
-
import {
|
|
4
|
+
import type { CompiledEventExecutor, Hooks } from "alepha";
|
|
5
|
+
import { $inject, Alepha, isFileLike, isTypeFile, t } from "alepha";
|
|
4
6
|
import { $logger } from "alepha/logger";
|
|
5
7
|
import { RouterProvider } from "alepha/router";
|
|
6
8
|
import type { RouteMethod } from "../constants/routeMethods.ts";
|
|
@@ -19,11 +21,12 @@ import { ServerRequestParser } from "../services/ServerRequestParser.ts";
|
|
|
19
21
|
import { ServerTimingProvider } from "./ServerTimingProvider.ts";
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
|
-
* Main router for all routes
|
|
24
|
+
* Main router for all routes server side.
|
|
23
25
|
*
|
|
26
|
+
* Reminder:
|
|
24
27
|
* - $route => generic route
|
|
25
28
|
* - $action => action route (for API calls)
|
|
26
|
-
* - $page => React route (for SSR)
|
|
29
|
+
* - $page => React route (for React SSR)
|
|
27
30
|
*/
|
|
28
31
|
export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
|
|
29
32
|
protected readonly log = $logger();
|
|
@@ -32,6 +35,66 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
|
|
|
32
35
|
protected readonly serverTimingProvider = $inject(ServerTimingProvider);
|
|
33
36
|
protected readonly serverRequestParser = $inject(ServerRequestParser);
|
|
34
37
|
|
|
38
|
+
// Compiled event executors - initialized on first request
|
|
39
|
+
protected compiledOnRequest?: CompiledEventExecutor<
|
|
40
|
+
Hooks["server:onRequest"]
|
|
41
|
+
>;
|
|
42
|
+
protected compiledOnSend?: CompiledEventExecutor<Hooks["server:onSend"]>;
|
|
43
|
+
protected compiledOnResponse?: CompiledEventExecutor<
|
|
44
|
+
Hooks["server:onResponse"]
|
|
45
|
+
>;
|
|
46
|
+
protected compiledOnError?: CompiledEventExecutor<Hooks["server:onError"]>;
|
|
47
|
+
|
|
48
|
+
// Reusable context.run options object - mutated per request
|
|
49
|
+
// Includes slots for request data to avoid closure allocation in context.run
|
|
50
|
+
protected readonly contextRunOptions = {
|
|
51
|
+
context: "",
|
|
52
|
+
// Request data slots - populated before context.run, read by processRequestBound
|
|
53
|
+
_request: null as unknown as ServerRequest,
|
|
54
|
+
_route: null as unknown as ServerRoute,
|
|
55
|
+
_responseKind: "any" as ResponseKind,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Pre-bound method reference - created once at instantiation, reused for all requests
|
|
59
|
+
// Reads arguments from contextRunOptions to avoid closure allocation per request
|
|
60
|
+
protected readonly processRequestBound = (): Promise<any> => {
|
|
61
|
+
const opts = this.contextRunOptions;
|
|
62
|
+
return this.processRequest(opts._request, opts._route, opts._responseKind);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Cache query schema keys to avoid property enumeration on every request
|
|
66
|
+
// WeakMap allows GC of schemas that are no longer referenced
|
|
67
|
+
protected readonly queryKeysCache = new WeakMap<object, string[]>();
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get cached keys for a query schema, computing them lazily on first access.
|
|
71
|
+
*/
|
|
72
|
+
protected getQuerySchemaKeys(schema: { properties: object }): string[] {
|
|
73
|
+
let keys = this.queryKeysCache.get(schema.properties);
|
|
74
|
+
if (!keys) {
|
|
75
|
+
keys = Object.keys(schema.properties);
|
|
76
|
+
this.queryKeysCache.set(schema.properties, keys);
|
|
77
|
+
}
|
|
78
|
+
return keys;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Compile event executors for optimal performance.
|
|
83
|
+
* Called lazily on first request after all hooks are registered.
|
|
84
|
+
*/
|
|
85
|
+
protected compileEvents(): void {
|
|
86
|
+
if (this.compiledOnRequest) return; // Already compiled
|
|
87
|
+
|
|
88
|
+
this.compiledOnRequest = this.alepha.events.compile("server:onRequest");
|
|
89
|
+
this.compiledOnSend = this.alepha.events.compile("server:onSend", {
|
|
90
|
+
catch: true,
|
|
91
|
+
});
|
|
92
|
+
this.compiledOnResponse = this.alepha.events.compile("server:onResponse", {
|
|
93
|
+
catch: true,
|
|
94
|
+
});
|
|
95
|
+
this.compiledOnError = this.alepha.events.compile("server:onError");
|
|
96
|
+
}
|
|
97
|
+
|
|
35
98
|
/**
|
|
36
99
|
* Get all registered routes, optionally filtered by a pattern.
|
|
37
100
|
*
|
|
@@ -52,6 +115,9 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
|
|
|
52
115
|
return this.routes;
|
|
53
116
|
}
|
|
54
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Create a new server route.
|
|
120
|
+
*/
|
|
55
121
|
public createRoute<TConfig extends RequestConfigSchema = RequestConfigSchema>(
|
|
56
122
|
route: ServerRoute<TConfig>,
|
|
57
123
|
): void {
|
|
@@ -71,69 +137,87 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
|
|
|
71
137
|
const request =
|
|
72
138
|
this.serverRequestParser.createServerRequest(rawRequest);
|
|
73
139
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
140
|
+
// Populate pre-allocated options with request data
|
|
141
|
+
// This avoids closure allocation - processRequestBound reads from these slots
|
|
142
|
+
const opts = this.contextRunOptions;
|
|
143
|
+
opts.context = this.getContextId(rawRequest.headers);
|
|
144
|
+
opts._request = request;
|
|
145
|
+
opts._route = route;
|
|
146
|
+
opts._responseKind = responseKind;
|
|
147
|
+
|
|
148
|
+
// Use pre-bound method reference instead of creating closure per request
|
|
149
|
+
return this.alepha.context.run(this.processRequestBound, opts);
|
|
80
150
|
},
|
|
81
151
|
});
|
|
82
152
|
}
|
|
83
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Get or generate a context ID from request headers.
|
|
156
|
+
*/
|
|
84
157
|
protected getContextId(headers: Record<string, string>): string {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
158
|
+
// Use constant header names to reduce string allocation
|
|
159
|
+
const contextId =
|
|
160
|
+
headers[HEADER_REQUEST_ID] || headers[HEADER_CORRELATION_ID];
|
|
161
|
+
if (contextId) {
|
|
89
162
|
return contextId;
|
|
90
163
|
}
|
|
91
164
|
|
|
92
|
-
return
|
|
165
|
+
return randomUUID();
|
|
93
166
|
}
|
|
94
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Process an incoming request through the full lifecycle:
|
|
170
|
+
* - onRequest hooks
|
|
171
|
+
* - route handler
|
|
172
|
+
* - onSend hooks
|
|
173
|
+
* - response serialization
|
|
174
|
+
* - onResponse hooks
|
|
175
|
+
*/
|
|
95
176
|
protected async processRequest(
|
|
96
177
|
request: ServerRequest,
|
|
97
178
|
route: ServerRoute,
|
|
98
179
|
responseKind: ResponseKind,
|
|
99
180
|
) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
});
|
|
181
|
+
// Compile events on first request (after all hooks are registered)
|
|
182
|
+
this.compileEvents();
|
|
103
183
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
184
|
+
// Use try/catch instead of .catch() to avoid function creation overhead
|
|
185
|
+
try {
|
|
186
|
+
await this.runRouteHandler(route, request, responseKind);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
await this.errorHandler(route, request, error as Error);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Local reference to reduce property lookups
|
|
192
|
+
const reply = request.reply;
|
|
193
|
+
|
|
194
|
+
// Create payload per request to avoid race conditions with concurrent requests
|
|
195
|
+
// (async hooks may hold references while other requests modify shared payloads)
|
|
196
|
+
const payload = { request, route, response: undefined as any };
|
|
114
197
|
|
|
115
|
-
//
|
|
198
|
+
// Use compiled executor - only await if returns promise
|
|
199
|
+
const onSendResult = this.compiledOnSend!(payload);
|
|
200
|
+
if (onSendResult) await onSendResult;
|
|
201
|
+
|
|
202
|
+
// Create response
|
|
116
203
|
const response = {
|
|
117
|
-
status:
|
|
118
|
-
headers:
|
|
119
|
-
body:
|
|
204
|
+
status: reply.status ?? (reply.body ? 200 : 204),
|
|
205
|
+
headers: reply.headers,
|
|
206
|
+
body: reply.body as any,
|
|
120
207
|
};
|
|
121
208
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
response,
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
catch: true, // avoid unhandled rejection
|
|
131
|
-
},
|
|
132
|
-
);
|
|
209
|
+
payload.response = response;
|
|
210
|
+
|
|
211
|
+
// Use compiled executor - only await if returns promise
|
|
212
|
+
const onResponseResult = this.compiledOnResponse!(payload);
|
|
213
|
+
if (onResponseResult) await onResponseResult;
|
|
133
214
|
|
|
134
215
|
return response;
|
|
135
216
|
}
|
|
136
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Run the route handler with request validation and response serialization.
|
|
220
|
+
*/
|
|
137
221
|
protected async runRouteHandler(
|
|
138
222
|
route: ServerRoute,
|
|
139
223
|
request: ServerRequest,
|
|
@@ -144,67 +228,73 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
|
|
|
144
228
|
// - ServerSecurityProvider (build user from headers)
|
|
145
229
|
// - ServerLoggerProvider (log request)
|
|
146
230
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
{
|
|
150
|
-
request,
|
|
151
|
-
route,
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
log: false,
|
|
155
|
-
},
|
|
156
|
-
);
|
|
231
|
+
// Local reference for timing provider
|
|
232
|
+
const timing = this.serverTimingProvider;
|
|
157
233
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
234
|
+
// Create payload per request to avoid race conditions with concurrent requests
|
|
235
|
+
const payload = { request, route };
|
|
236
|
+
|
|
237
|
+
// Use compiled executor - only await if returns promise
|
|
238
|
+
const onRequestResult = this.compiledOnRequest!(payload);
|
|
239
|
+
if (onRequestResult) await onRequestResult;
|
|
240
|
+
|
|
241
|
+
// Local reference to reduce property lookups
|
|
242
|
+
const reply = request.reply;
|
|
243
|
+
if (reply.body || (reply.status && reply.status >= 200)) {
|
|
162
244
|
// if the body is already set, we can skip the handler
|
|
163
245
|
// this is useful for middlewares that set the body
|
|
164
246
|
return;
|
|
165
247
|
}
|
|
166
248
|
|
|
167
249
|
// request is ready to be used -> inject to context
|
|
168
|
-
this.alepha.context.set<ServerRequest>(
|
|
250
|
+
this.alepha.context.set<ServerRequest>(CTX_REQUEST, request);
|
|
169
251
|
|
|
170
252
|
// validate request
|
|
171
|
-
|
|
253
|
+
timing.beginTiming(TIMING_VALIDATE);
|
|
172
254
|
try {
|
|
173
255
|
this.validateRequest(route, request);
|
|
174
256
|
} finally {
|
|
175
|
-
|
|
257
|
+
timing.endTiming(TIMING_VALIDATE);
|
|
176
258
|
}
|
|
177
259
|
|
|
178
260
|
// call the handler only if the body is not set yet
|
|
179
|
-
|
|
261
|
+
timing.beginTiming(TIMING_HANDLER);
|
|
180
262
|
try {
|
|
181
263
|
const result = await route.handler(request);
|
|
182
264
|
if (result) {
|
|
183
265
|
request.reply.body = result;
|
|
184
266
|
}
|
|
185
267
|
} finally {
|
|
186
|
-
|
|
268
|
+
timing.endTiming(TIMING_HANDLER);
|
|
187
269
|
}
|
|
188
270
|
|
|
189
271
|
// serialize response
|
|
190
|
-
|
|
272
|
+
timing.beginTiming(TIMING_SERIALIZE);
|
|
191
273
|
try {
|
|
192
274
|
this.serializeResponse(route, request.reply, responseKind);
|
|
193
275
|
} finally {
|
|
194
|
-
|
|
276
|
+
timing.endTiming(TIMING_SERIALIZE);
|
|
195
277
|
}
|
|
196
278
|
}
|
|
197
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Transform reply body based on response kind and route schema.
|
|
282
|
+
*/
|
|
198
283
|
public serializeResponse(
|
|
199
284
|
route: ServerRoute,
|
|
200
285
|
reply: ServerReply,
|
|
201
286
|
responseKind: ResponseKind,
|
|
202
287
|
): void {
|
|
288
|
+
// Local reference to reduce property lookups
|
|
289
|
+
const headers = reply.headers;
|
|
290
|
+
|
|
203
291
|
if (responseKind === "json" && route.schema?.response) {
|
|
204
|
-
|
|
205
|
-
reply.body = this.alepha.codec.encode(
|
|
206
|
-
|
|
207
|
-
|
|
292
|
+
headers[HEADER_CONTENT_TYPE] = CONTENT_TYPE_JSON;
|
|
293
|
+
reply.body = this.alepha.codec.encode(
|
|
294
|
+
route.schema.response,
|
|
295
|
+
reply.body,
|
|
296
|
+
ENCODE_OPTIONS_STRING,
|
|
297
|
+
);
|
|
208
298
|
return;
|
|
209
299
|
}
|
|
210
300
|
|
|
@@ -212,11 +302,11 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
|
|
|
212
302
|
if (!isFileLike(reply.body)) {
|
|
213
303
|
throw new HttpError({
|
|
214
304
|
status: 500,
|
|
215
|
-
message:
|
|
305
|
+
message: ERROR_NOT_FILE,
|
|
216
306
|
});
|
|
217
307
|
}
|
|
218
|
-
|
|
219
|
-
|
|
308
|
+
headers[HEADER_CONTENT_TYPE] = reply.body.type;
|
|
309
|
+
headers[HEADER_CONTENT_DISPOSITION] =
|
|
220
310
|
`attachment; filename="${reply.body.name.replaceAll('"', "")}"`;
|
|
221
311
|
reply.body = reply.body.stream();
|
|
222
312
|
return;
|
|
@@ -224,22 +314,26 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
|
|
|
224
314
|
|
|
225
315
|
if (responseKind === "text") {
|
|
226
316
|
reply.body = String(reply.body);
|
|
227
|
-
if (
|
|
228
|
-
reply.
|
|
317
|
+
if (
|
|
318
|
+
reply.body.length > 15 &&
|
|
319
|
+
reply.body.charCodeAt(0) === 60 &&
|
|
320
|
+
reply.body.startsWith("<!DOCTYPE html>")
|
|
321
|
+
) {
|
|
322
|
+
headers[HEADER_CONTENT_TYPE] ??= CONTENT_TYPE_HTML;
|
|
229
323
|
} else {
|
|
230
|
-
|
|
324
|
+
headers[HEADER_CONTENT_TYPE] ??= CONTENT_TYPE_TEXT;
|
|
231
325
|
}
|
|
232
326
|
return;
|
|
233
327
|
}
|
|
234
328
|
|
|
235
329
|
if (reply.body == null || responseKind === "void") {
|
|
236
|
-
delete
|
|
330
|
+
delete headers[HEADER_CONTENT_TYPE];
|
|
237
331
|
reply.body = undefined;
|
|
238
332
|
return;
|
|
239
333
|
}
|
|
240
334
|
|
|
241
335
|
if (Buffer.isBuffer(reply.body)) {
|
|
242
|
-
|
|
336
|
+
headers[HEADER_CONTENT_TYPE] ??= CONTENT_TYPE_OCTET;
|
|
243
337
|
return;
|
|
244
338
|
}
|
|
245
339
|
|
|
@@ -248,15 +342,18 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
|
|
|
248
342
|
reply.body instanceof NodeStream
|
|
249
343
|
) {
|
|
250
344
|
// set content-type to application/octet-stream if not set
|
|
251
|
-
|
|
345
|
+
headers[HEADER_CONTENT_TYPE] ??= CONTENT_TYPE_OCTET;
|
|
252
346
|
return;
|
|
253
347
|
}
|
|
254
348
|
|
|
255
|
-
|
|
349
|
+
headers[HEADER_CONTENT_TYPE] ??= CONTENT_TYPE_TEXT;
|
|
256
350
|
reply.body = String(reply.body);
|
|
257
351
|
return;
|
|
258
352
|
}
|
|
259
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Determine response type based on route schema.
|
|
356
|
+
*/
|
|
260
357
|
protected getResponseType(schema?: RequestConfigSchema): ResponseKind {
|
|
261
358
|
if (schema?.response) {
|
|
262
359
|
if (
|
|
@@ -288,68 +385,75 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
|
|
|
288
385
|
return "any";
|
|
289
386
|
}
|
|
290
387
|
|
|
388
|
+
/**
|
|
389
|
+
* When an error occurs during request processing, this method is called.
|
|
390
|
+
*/
|
|
291
391
|
protected async errorHandler(
|
|
292
392
|
route: ServerRoute,
|
|
293
393
|
request: ServerRequest,
|
|
294
394
|
error: Error,
|
|
295
395
|
) {
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
);
|
|
396
|
+
// Local references to reduce property lookups
|
|
397
|
+
const reply = request.reply;
|
|
398
|
+
const headers = reply.headers;
|
|
399
|
+
const requestId = request.requestId;
|
|
400
|
+
|
|
401
|
+
// Reset body, which is probably invalid!
|
|
402
|
+
// It can be filled by server:onError hook or by the default handler below
|
|
403
|
+
reply.body = null;
|
|
404
|
+
|
|
405
|
+
// Use compiled executor - only await if returns promise
|
|
406
|
+
const onErrorResult = this.compiledOnError!({ request, route, error });
|
|
407
|
+
if (onErrorResult) {
|
|
408
|
+
await onErrorResult;
|
|
409
|
+
}
|
|
311
410
|
|
|
312
|
-
if (!
|
|
411
|
+
if (!reply.body && !reply.status) {
|
|
313
412
|
if (error instanceof HttpError) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
413
|
+
reply.status = error.status;
|
|
414
|
+
headers[HEADER_CONTENT_TYPE] = CONTENT_TYPE_JSON;
|
|
415
|
+
// Avoid spread operator - directly mutate the error JSON
|
|
416
|
+
const errorJson = HttpError.toJSON(error);
|
|
417
|
+
errorJson.requestId = requestId;
|
|
418
|
+
reply.body = JSON.stringify(errorJson);
|
|
320
419
|
} else {
|
|
321
420
|
if (
|
|
322
421
|
"status" in error &&
|
|
323
422
|
typeof error.status === "number" &&
|
|
324
423
|
!!errorNameByStatus[error.status]
|
|
325
424
|
) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
425
|
+
const status = error.status;
|
|
426
|
+
reply.status = status;
|
|
427
|
+
headers[HEADER_CONTENT_TYPE] = CONTENT_TYPE_JSON;
|
|
428
|
+
reply.body = JSON.stringify({
|
|
429
|
+
status,
|
|
430
|
+
error: errorNameByStatus[status],
|
|
431
|
+
message: error.message,
|
|
432
|
+
requestId,
|
|
333
433
|
});
|
|
334
434
|
return;
|
|
335
435
|
}
|
|
336
436
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
437
|
+
reply.status = 500;
|
|
438
|
+
headers[HEADER_CONTENT_TYPE] = CONTENT_TYPE_JSON;
|
|
439
|
+
reply.body = JSON.stringify({
|
|
340
440
|
status: 500,
|
|
341
|
-
error:
|
|
342
|
-
message:
|
|
343
|
-
requestId
|
|
441
|
+
error: ERROR_INTERNAL,
|
|
442
|
+
message: error.message,
|
|
443
|
+
requestId,
|
|
344
444
|
});
|
|
345
445
|
}
|
|
346
446
|
}
|
|
347
447
|
}
|
|
348
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Validate incoming request against route schema.
|
|
451
|
+
*/
|
|
349
452
|
public validateRequest(
|
|
350
453
|
route: { schema?: RequestConfigSchema },
|
|
351
454
|
request: ServerRequestConfig,
|
|
352
455
|
) {
|
|
456
|
+
// Validate params (path parameters)
|
|
353
457
|
if (route.schema?.params) {
|
|
354
458
|
try {
|
|
355
459
|
request.params = this.alepha.codec.validate(
|
|
@@ -361,24 +465,32 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
|
|
|
361
465
|
}
|
|
362
466
|
}
|
|
363
467
|
|
|
468
|
+
// Validate query parameters (?key=value&key2=value2)
|
|
364
469
|
if (route.schema?.query) {
|
|
365
470
|
try {
|
|
366
|
-
//
|
|
471
|
+
// Use cached keys instead of for...in enumeration
|
|
472
|
+
const schemaQuery = route.schema.query;
|
|
473
|
+
const keys = this.getQuerySchemaKeys(schemaQuery);
|
|
367
474
|
const query: Record<string, any> = {};
|
|
368
|
-
|
|
475
|
+
|
|
476
|
+
// Use indexed loop for better performance than for...of
|
|
477
|
+
for (let i = 0; i < keys.length; i++) {
|
|
478
|
+
const key = keys[i];
|
|
369
479
|
if (request.query[key] != null) {
|
|
370
480
|
query[key] = this.alepha.codec.decode(
|
|
371
|
-
|
|
481
|
+
schemaQuery.properties[key],
|
|
372
482
|
request.query[key],
|
|
373
483
|
);
|
|
374
484
|
}
|
|
375
485
|
}
|
|
486
|
+
|
|
376
487
|
request.query = query;
|
|
377
488
|
} catch (error) {
|
|
378
489
|
throw new ValidationError("Invalid request query", error);
|
|
379
490
|
}
|
|
380
491
|
}
|
|
381
492
|
|
|
493
|
+
// Validate headers
|
|
382
494
|
if (route.schema?.headers) {
|
|
383
495
|
try {
|
|
384
496
|
request.headers = this.alepha.codec.validate(
|
|
@@ -390,6 +502,7 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
|
|
|
390
502
|
}
|
|
391
503
|
}
|
|
392
504
|
|
|
505
|
+
// Validate body
|
|
393
506
|
if (route.schema?.body) {
|
|
394
507
|
if (t.schema.isString(route.schema.body)) {
|
|
395
508
|
if (typeof request.body !== "string") {
|
|
@@ -408,3 +521,34 @@ export class ServerRouterProvider extends RouterProvider<ServerRouteMatcher> {
|
|
|
408
521
|
}
|
|
409
522
|
}
|
|
410
523
|
}
|
|
524
|
+
|
|
525
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
526
|
+
|
|
527
|
+
// Pre-allocated encode options for response serialization
|
|
528
|
+
const ENCODE_OPTIONS_STRING = Object.freeze({
|
|
529
|
+
as: "string" as const,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// HTTP Headers
|
|
533
|
+
const HEADER_CONTENT_TYPE = "content-type";
|
|
534
|
+
const HEADER_CONTENT_DISPOSITION = "content-disposition";
|
|
535
|
+
const HEADER_REQUEST_ID = "x-request-id";
|
|
536
|
+
const HEADER_CORRELATION_ID = "x-correlation-id";
|
|
537
|
+
|
|
538
|
+
// Content Types
|
|
539
|
+
const CONTENT_TYPE_JSON = "application/json";
|
|
540
|
+
const CONTENT_TYPE_TEXT = "text/plain";
|
|
541
|
+
const CONTENT_TYPE_HTML = "text/html; charset=UTF-8";
|
|
542
|
+
const CONTENT_TYPE_OCTET = "application/octet-stream";
|
|
543
|
+
|
|
544
|
+
// Timing Keys
|
|
545
|
+
const TIMING_VALIDATE = "validateRequest";
|
|
546
|
+
const TIMING_HANDLER = "runHandler";
|
|
547
|
+
const TIMING_SERIALIZE = "serializeResponse";
|
|
548
|
+
|
|
549
|
+
// Context Keys
|
|
550
|
+
const CTX_REQUEST = "request";
|
|
551
|
+
|
|
552
|
+
// Error Messages
|
|
553
|
+
const ERROR_INTERNAL = "InternalServerError";
|
|
554
|
+
const ERROR_NOT_FILE = "Invalid response body - not a file";
|
|
@@ -18,7 +18,7 @@ export class ServerTimingProvider {
|
|
|
18
18
|
public readonly onRequest = $hook({
|
|
19
19
|
priority: "first",
|
|
20
20
|
on: "server:onRequest",
|
|
21
|
-
handler:
|
|
21
|
+
handler: ({ request }) => {
|
|
22
22
|
if (this.options.disabled) {
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
@@ -31,7 +31,7 @@ export class ServerTimingProvider {
|
|
|
31
31
|
public readonly onResponse = $hook({
|
|
32
32
|
priority: "last",
|
|
33
33
|
on: "server:onResponse",
|
|
34
|
-
handler:
|
|
34
|
+
handler: ({ request }) => {
|
|
35
35
|
if (this.options.disabled) {
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
t,
|
|
8
8
|
} from "alepha";
|
|
9
9
|
import { $logger } from "alepha/logger";
|
|
10
|
+
import type { ServerRouteSecure } from "alepha/security";
|
|
10
11
|
import {
|
|
11
12
|
type ActionPrimitive,
|
|
12
13
|
type ClientRequestEntry,
|
|
@@ -22,7 +23,6 @@ import {
|
|
|
22
23
|
type TRequestBody,
|
|
23
24
|
UnauthorizedError,
|
|
24
25
|
} from "alepha/server";
|
|
25
|
-
import type { ServerRouteSecure } from "alepha/server/security";
|
|
26
26
|
import {
|
|
27
27
|
type ApiLink,
|
|
28
28
|
apiLinksResponseSchema,
|