alepha 0.12.1 → 0.13.1
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/dist/api-notifications/index.d.ts +111 -111
- package/dist/api-users/index.d.ts +1240 -1240
- package/dist/api-verifications/index.d.ts +94 -94
- package/dist/cli/{dist-Sz2EXvQX.cjs → dist-Dl9Vl7Ur.js} +17 -13
- package/dist/cli/{dist-BBPjuQ56.js.map → dist-Dl9Vl7Ur.js.map} +1 -1
- package/dist/cli/index.d.ts +3 -11
- package/dist/cli/index.js +106 -74
- package/dist/cli/index.js.map +1 -1
- package/dist/email/index.js +71 -73
- package/dist/email/index.js.map +1 -1
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/index.d.ts +4 -4
- package/dist/redis/index.d.ts +10 -10
- package/dist/retry/index.d.ts +1 -1
- package/dist/retry/index.js +2 -2
- package/dist/retry/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +6 -6
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server-auth/index.d.ts +193 -193
- package/dist/server-health/index.d.ts +17 -17
- package/dist/server-links/index.d.ts +34 -34
- package/dist/server-metrics/index.js +170 -174
- package/dist/server-metrics/index.js.map +1 -1
- package/dist/server-security/index.d.ts +9 -9
- package/dist/vite/index.js +4 -5
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.d.ts +7 -7
- package/package.json +52 -103
- package/src/cli/apps/AlephaPackageBuilderCli.ts +7 -2
- package/src/cli/assets/appRouterTs.ts +9 -0
- package/src/cli/assets/indexHtml.ts +2 -1
- package/src/cli/assets/mainBrowserTs.ts +10 -0
- package/src/cli/commands/CoreCommands.ts +6 -5
- package/src/cli/commands/DrizzleCommands.ts +65 -57
- package/src/cli/commands/VerifyCommands.ts +1 -1
- package/src/cli/services/ProjectUtils.ts +44 -38
- package/src/orm/providers/DrizzleKitProvider.ts +1 -1
- package/src/retry/descriptors/$retry.ts +5 -3
- package/src/server/providers/NodeHttpServerProvider.ts +1 -1
- package/src/vite/helpers/boot.ts +3 -3
- package/dist/api-files/index.cjs +0 -1293
- package/dist/api-files/index.cjs.map +0 -1
- package/dist/api-files/index.d.cts +0 -829
- package/dist/api-jobs/index.cjs +0 -274
- package/dist/api-jobs/index.cjs.map +0 -1
- package/dist/api-jobs/index.d.cts +0 -654
- package/dist/api-notifications/index.cjs +0 -380
- package/dist/api-notifications/index.cjs.map +0 -1
- package/dist/api-notifications/index.d.cts +0 -289
- package/dist/api-parameters/index.cjs +0 -66
- package/dist/api-parameters/index.cjs.map +0 -1
- package/dist/api-parameters/index.d.cts +0 -84
- package/dist/api-users/index.cjs +0 -6009
- package/dist/api-users/index.cjs.map +0 -1
- package/dist/api-users/index.d.cts +0 -4740
- package/dist/api-verifications/index.cjs +0 -407
- package/dist/api-verifications/index.cjs.map +0 -1
- package/dist/api-verifications/index.d.cts +0 -207
- package/dist/batch/index.cjs +0 -408
- package/dist/batch/index.cjs.map +0 -1
- package/dist/batch/index.d.cts +0 -330
- package/dist/bin/index.cjs +0 -17
- package/dist/bin/index.cjs.map +0 -1
- package/dist/bin/index.d.cts +0 -1
- package/dist/bucket/index.cjs +0 -303
- package/dist/bucket/index.cjs.map +0 -1
- package/dist/bucket/index.d.cts +0 -355
- package/dist/cache/index.cjs +0 -241
- package/dist/cache/index.cjs.map +0 -1
- package/dist/cache/index.d.cts +0 -202
- package/dist/cache-redis/index.cjs +0 -84
- package/dist/cache-redis/index.cjs.map +0 -1
- package/dist/cache-redis/index.d.cts +0 -40
- package/dist/cli/chunk-DSlc6foC.cjs +0 -43
- package/dist/cli/dist-BBPjuQ56.js +0 -2778
- package/dist/cli/dist-Sz2EXvQX.cjs.map +0 -1
- package/dist/cli/index.cjs +0 -1241
- package/dist/cli/index.cjs.map +0 -1
- package/dist/cli/index.d.cts +0 -422
- package/dist/command/index.cjs +0 -693
- package/dist/command/index.cjs.map +0 -1
- package/dist/command/index.d.cts +0 -340
- package/dist/core/index.cjs +0 -2264
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -1927
- package/dist/datetime/index.cjs +0 -318
- package/dist/datetime/index.cjs.map +0 -1
- package/dist/datetime/index.d.cts +0 -145
- package/dist/email/index.cjs +0 -10874
- package/dist/email/index.cjs.map +0 -1
- package/dist/email/index.d.cts +0 -186
- package/dist/fake/index.cjs +0 -34641
- package/dist/fake/index.cjs.map +0 -1
- package/dist/fake/index.d.cts +0 -74
- package/dist/file/index.cjs +0 -1212
- package/dist/file/index.cjs.map +0 -1
- package/dist/file/index.d.cts +0 -698
- package/dist/lock/index.cjs +0 -226
- package/dist/lock/index.cjs.map +0 -1
- package/dist/lock/index.d.cts +0 -361
- package/dist/lock-redis/index.cjs +0 -113
- package/dist/lock-redis/index.cjs.map +0 -1
- package/dist/lock-redis/index.d.cts +0 -24
- package/dist/logger/index.cjs +0 -521
- package/dist/logger/index.cjs.map +0 -1
- package/dist/logger/index.d.cts +0 -281
- package/dist/orm/index.cjs +0 -2986
- package/dist/orm/index.cjs.map +0 -1
- package/dist/orm/index.d.cts +0 -2213
- package/dist/queue/index.cjs +0 -1044
- package/dist/queue/index.cjs.map +0 -1
- package/dist/queue/index.d.cts +0 -1265
- package/dist/queue-redis/index.cjs +0 -873
- package/dist/queue-redis/index.cjs.map +0 -1
- package/dist/queue-redis/index.d.cts +0 -82
- package/dist/redis/index.cjs +0 -153
- package/dist/redis/index.cjs.map +0 -1
- package/dist/redis/index.d.cts +0 -82
- package/dist/retry/index.cjs +0 -146
- package/dist/retry/index.cjs.map +0 -1
- package/dist/retry/index.d.cts +0 -172
- package/dist/router/index.cjs +0 -111
- package/dist/router/index.cjs.map +0 -1
- package/dist/router/index.d.cts +0 -46
- package/dist/scheduler/index.cjs +0 -576
- package/dist/scheduler/index.cjs.map +0 -1
- package/dist/scheduler/index.d.cts +0 -145
- package/dist/security/index.cjs +0 -2402
- package/dist/security/index.cjs.map +0 -1
- package/dist/security/index.d.cts +0 -598
- package/dist/server/index.cjs +0 -1680
- package/dist/server/index.cjs.map +0 -1
- package/dist/server/index.d.cts +0 -810
- package/dist/server-auth/index.cjs +0 -3146
- package/dist/server-auth/index.cjs.map +0 -1
- package/dist/server-auth/index.d.cts +0 -1164
- package/dist/server-cache/index.cjs +0 -252
- package/dist/server-cache/index.cjs.map +0 -1
- package/dist/server-cache/index.d.cts +0 -164
- package/dist/server-compress/index.cjs +0 -141
- package/dist/server-compress/index.cjs.map +0 -1
- package/dist/server-compress/index.d.cts +0 -38
- package/dist/server-cookies/index.cjs +0 -234
- package/dist/server-cookies/index.cjs.map +0 -1
- package/dist/server-cookies/index.d.cts +0 -144
- package/dist/server-cors/index.cjs +0 -201
- package/dist/server-cors/index.cjs.map +0 -1
- package/dist/server-cors/index.d.cts +0 -140
- package/dist/server-health/index.cjs +0 -62
- package/dist/server-health/index.cjs.map +0 -1
- package/dist/server-health/index.d.cts +0 -58
- package/dist/server-helmet/index.cjs +0 -131
- package/dist/server-helmet/index.cjs.map +0 -1
- package/dist/server-helmet/index.d.cts +0 -97
- package/dist/server-links/index.cjs +0 -992
- package/dist/server-links/index.cjs.map +0 -1
- package/dist/server-links/index.d.cts +0 -513
- package/dist/server-metrics/index.cjs +0 -4535
- package/dist/server-metrics/index.cjs.map +0 -1
- package/dist/server-metrics/index.d.cts +0 -35
- package/dist/server-multipart/index.cjs +0 -237
- package/dist/server-multipart/index.cjs.map +0 -1
- package/dist/server-multipart/index.d.cts +0 -50
- package/dist/server-proxy/index.cjs +0 -186
- package/dist/server-proxy/index.cjs.map +0 -1
- package/dist/server-proxy/index.d.cts +0 -234
- package/dist/server-rate-limit/index.cjs +0 -241
- package/dist/server-rate-limit/index.cjs.map +0 -1
- package/dist/server-rate-limit/index.d.cts +0 -183
- package/dist/server-security/index.cjs +0 -316
- package/dist/server-security/index.cjs.map +0 -1
- package/dist/server-security/index.d.cts +0 -173
- package/dist/server-static/index.cjs +0 -170
- package/dist/server-static/index.cjs.map +0 -1
- package/dist/server-static/index.d.cts +0 -121
- package/dist/server-swagger/index.cjs +0 -1021
- package/dist/server-swagger/index.cjs.map +0 -1
- package/dist/server-swagger/index.d.cts +0 -382
- package/dist/sms/index.cjs +0 -221
- package/dist/sms/index.cjs.map +0 -1
- package/dist/sms/index.d.cts +0 -130
- package/dist/thread/index.cjs +0 -350
- package/dist/thread/index.cjs.map +0 -1
- package/dist/thread/index.d.cts +0 -260
- package/dist/topic/index.cjs +0 -282
- package/dist/topic/index.cjs.map +0 -1
- package/dist/topic/index.d.cts +0 -523
- package/dist/topic-redis/index.cjs +0 -71
- package/dist/topic-redis/index.cjs.map +0 -1
- package/dist/topic-redis/index.d.cts +0 -42
- package/dist/vite/index.cjs +0 -1077
- package/dist/vite/index.cjs.map +0 -1
- package/dist/vite/index.d.cts +0 -542
- package/dist/websocket/index.cjs +0 -1117
- package/dist/websocket/index.cjs.map +0 -1
- package/dist/websocket/index.d.cts +0 -861
package/dist/server/index.cjs
DELETED
|
@@ -1,1680 +0,0 @@
|
|
|
1
|
-
let alepha = require("alepha");
|
|
2
|
-
let alepha_logger = require("alepha/logger");
|
|
3
|
-
let node_stream = require("node:stream");
|
|
4
|
-
let alepha_datetime = require("alepha/datetime");
|
|
5
|
-
let node_stream_web = require("node:stream/web");
|
|
6
|
-
let alepha_router = require("alepha/router");
|
|
7
|
-
let alepha_cache = require("alepha/cache");
|
|
8
|
-
let node_http = require("node:http");
|
|
9
|
-
let node_zlib = require("node:zlib");
|
|
10
|
-
|
|
11
|
-
//#region src/server/helpers/isMultipart.ts
|
|
12
|
-
/**
|
|
13
|
-
* Checks if the route has multipart/form-data request body.
|
|
14
|
-
*/
|
|
15
|
-
const isMultipart = (options) => {
|
|
16
|
-
if (options.requestBodyType === "multipart/form-data") return true;
|
|
17
|
-
if (options.schema?.body && "properties" in options.schema.body) {
|
|
18
|
-
const properties = options.schema.body.properties;
|
|
19
|
-
for (const key in properties) if (properties[key].format === "binary") return true;
|
|
20
|
-
}
|
|
21
|
-
return false;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
//#endregion
|
|
25
|
-
//#region src/server/helpers/ServerReply.ts
|
|
26
|
-
/**
|
|
27
|
-
* Helper for building server replies.
|
|
28
|
-
*/
|
|
29
|
-
var ServerReply = class {
|
|
30
|
-
headers = {};
|
|
31
|
-
status;
|
|
32
|
-
body;
|
|
33
|
-
/**
|
|
34
|
-
* Redirect to a given URL with optional status code (default 302).
|
|
35
|
-
*/
|
|
36
|
-
redirect(url, status = 302) {
|
|
37
|
-
this.status = status;
|
|
38
|
-
this.headers.location = url;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Set the response status code.
|
|
42
|
-
*/
|
|
43
|
-
setStatus(status) {
|
|
44
|
-
this.status = status;
|
|
45
|
-
return this;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Set a response header.
|
|
49
|
-
*/
|
|
50
|
-
setHeader(name, value) {
|
|
51
|
-
this.headers[name.toLowerCase()] = value;
|
|
52
|
-
return this;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Set the response body.
|
|
56
|
-
*/
|
|
57
|
-
setBody(body) {
|
|
58
|
-
this.body = body;
|
|
59
|
-
return this;
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
//#endregion
|
|
64
|
-
//#region src/server/errors/HttpError.ts
|
|
65
|
-
const isHttpError = (error, status) => {
|
|
66
|
-
if (!(!!error && typeof error === "object" && "message" in error && typeof error.message === "string" && "status" in error && typeof error.status === "number")) return false;
|
|
67
|
-
if (status) return error.status === status;
|
|
68
|
-
return true;
|
|
69
|
-
};
|
|
70
|
-
var HttpError = class extends alepha.AlephaError {
|
|
71
|
-
name = "HttpError";
|
|
72
|
-
static is = isHttpError;
|
|
73
|
-
static toJSON(error) {
|
|
74
|
-
const json = {
|
|
75
|
-
error: error.error,
|
|
76
|
-
status: error.status,
|
|
77
|
-
message: error.message
|
|
78
|
-
};
|
|
79
|
-
if (error.details) json.details = error.details;
|
|
80
|
-
if (error.requestId) json.requestId = error.requestId;
|
|
81
|
-
if (error.reason) json.cause = error.reason;
|
|
82
|
-
return json;
|
|
83
|
-
}
|
|
84
|
-
error;
|
|
85
|
-
status;
|
|
86
|
-
requestId;
|
|
87
|
-
details;
|
|
88
|
-
reason;
|
|
89
|
-
constructor(options, cause) {
|
|
90
|
-
super(options.message, { cause });
|
|
91
|
-
this.status = options.status ?? 500;
|
|
92
|
-
this.details = options.details;
|
|
93
|
-
this.requestId = options.requestId;
|
|
94
|
-
if (typeof options.cause === "object") this.reason = {
|
|
95
|
-
name: options.cause.name,
|
|
96
|
-
message: options.cause.message
|
|
97
|
-
};
|
|
98
|
-
else if (cause instanceof Error) this.reason = {
|
|
99
|
-
name: cause.name,
|
|
100
|
-
message: cause.message
|
|
101
|
-
};
|
|
102
|
-
if (this.constructor.name === "HttpError") this.error = options.error ?? errorNameByStatus[this.status] ?? "HttpError";
|
|
103
|
-
else this.error = this.constructor.name;
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
const errorNameByStatus = {
|
|
107
|
-
400: "BadRequestError",
|
|
108
|
-
401: "UnauthorizedError",
|
|
109
|
-
403: "ForbiddenError",
|
|
110
|
-
404: "NotFoundError",
|
|
111
|
-
405: "MethodNotAllowedError",
|
|
112
|
-
409: "ConflictError",
|
|
113
|
-
410: "GoneError",
|
|
114
|
-
413: "PayloadTooLargeError",
|
|
115
|
-
415: "UnsupportedMediaTypeError",
|
|
116
|
-
429: "TooManyRequestsError",
|
|
117
|
-
500: "InternalServerError",
|
|
118
|
-
501: "NotImplementedError",
|
|
119
|
-
502: "BadGatewayError",
|
|
120
|
-
503: "ServiceUnavailableError",
|
|
121
|
-
504: "GatewayTimeoutError"
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
//#endregion
|
|
125
|
-
//#region src/server/errors/ValidationError.ts
|
|
126
|
-
var ValidationError = class extends HttpError {
|
|
127
|
-
constructor(message = "Validation has failed", cause) {
|
|
128
|
-
super({
|
|
129
|
-
message,
|
|
130
|
-
status: 400
|
|
131
|
-
}, cause);
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
//#endregion
|
|
136
|
-
//#region src/server/services/UserAgentParser.ts
|
|
137
|
-
/**
|
|
138
|
-
* Simple User-Agent parser to detect OS, browser, and device type.
|
|
139
|
-
* This parser is not exhaustive and may not cover all edge cases.
|
|
140
|
-
*
|
|
141
|
-
* Use result for non
|
|
142
|
-
*/
|
|
143
|
-
var UserAgentParser = class {
|
|
144
|
-
parse(userAgent = "") {
|
|
145
|
-
const ua = userAgent.toLowerCase();
|
|
146
|
-
let os = "Windows";
|
|
147
|
-
let browser = "Chrome";
|
|
148
|
-
let device = "DESKTOP";
|
|
149
|
-
if (ua.includes("windows phone")) os = "Windows Phone";
|
|
150
|
-
else if (ua.includes("windows")) os = "Windows";
|
|
151
|
-
else if (ua.includes("android")) os = "Android";
|
|
152
|
-
else if (ua.includes("iphone") || ua.includes("ipad") || ua.includes("ipod") || ua.includes("ios") && !ua.includes("android")) os = "iOS";
|
|
153
|
-
else if (ua.includes("mac os") || ua.includes("macos") || ua.includes("macintosh")) os = "MacOS";
|
|
154
|
-
else if (ua.includes("cros") || ua.includes("chromeos")) os = "ChromeOS";
|
|
155
|
-
else if (ua.includes("ubuntu")) os = "Ubuntu";
|
|
156
|
-
else if (ua.includes("freebsd")) os = "FreeBSD";
|
|
157
|
-
else if (ua.includes("openbsd")) os = "OpenBSD";
|
|
158
|
-
else if (ua.includes("blackberry") || ua.includes("bb10")) os = "BlackBerry";
|
|
159
|
-
else if (ua.includes("symbian") || ua.includes("symbos")) os = "Symbian";
|
|
160
|
-
else if (ua.includes("linux") || ua.includes("x11")) os = "Linux";
|
|
161
|
-
if (ua.includes("yabrowser") || ua.includes("yandex")) browser = "Yandex";
|
|
162
|
-
else if (ua.includes("brave")) browser = "Brave";
|
|
163
|
-
else if (ua.includes("vivaldi")) browser = "Vivaldi";
|
|
164
|
-
else if (ua.includes("samsungbrowser") || ua.includes("samsung")) browser = "Samsung Browser";
|
|
165
|
-
else if (ua.includes("ucbrowser") || ua.includes("uc browser")) browser = "UC Browser";
|
|
166
|
-
else if (ua.includes("opera") || ua.includes("opr/") || ua.includes("opios")) browser = "Opera";
|
|
167
|
-
else if (ua.includes("edg/") || ua.includes("edge") || ua.includes("edgios")) browser = "Edge";
|
|
168
|
-
else if (ua.includes("firefox") && !ua.includes("seamonkey")) browser = "Firefox";
|
|
169
|
-
else if (ua.includes("trident") || ua.includes("msie")) browser = "Internet Explorer";
|
|
170
|
-
else if (ua.includes("safari") && !ua.includes("chrome") && !ua.includes("chromium")) browser = "Safari";
|
|
171
|
-
else if (ua.includes("chrome") || ua.includes("chromium") || ua.includes("crios")) browser = "Chrome";
|
|
172
|
-
const mobileKeywords = [
|
|
173
|
-
"mobile",
|
|
174
|
-
"android",
|
|
175
|
-
"iphone",
|
|
176
|
-
"ipod",
|
|
177
|
-
"blackberry",
|
|
178
|
-
"windows phone",
|
|
179
|
-
"opera mini",
|
|
180
|
-
"iemobile",
|
|
181
|
-
"mobile safari",
|
|
182
|
-
"nokia",
|
|
183
|
-
"symbian"
|
|
184
|
-
];
|
|
185
|
-
const isTablet = [
|
|
186
|
-
"ipad",
|
|
187
|
-
"tablet",
|
|
188
|
-
"kindle",
|
|
189
|
-
"silk",
|
|
190
|
-
"gt-p",
|
|
191
|
-
"sm-t",
|
|
192
|
-
"nexus 7",
|
|
193
|
-
"nexus 10"
|
|
194
|
-
].some((keyword) => ua.includes(keyword));
|
|
195
|
-
const isMobile = mobileKeywords.some((keyword) => ua.includes(keyword));
|
|
196
|
-
if (isTablet) device = "TABLET";
|
|
197
|
-
else if (isMobile) device = "MOBILE";
|
|
198
|
-
else device = "DESKTOP";
|
|
199
|
-
return {
|
|
200
|
-
os,
|
|
201
|
-
browser,
|
|
202
|
-
device
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
//#endregion
|
|
208
|
-
//#region src/server/services/ServerRequestParser.ts
|
|
209
|
-
var ServerRequestParser = class {
|
|
210
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
211
|
-
userAgentParser = (0, alepha.$inject)(UserAgentParser);
|
|
212
|
-
createServerRequest(rawRequest) {
|
|
213
|
-
const self = this;
|
|
214
|
-
return {
|
|
215
|
-
method: rawRequest.method,
|
|
216
|
-
url: rawRequest.url,
|
|
217
|
-
raw: rawRequest.raw,
|
|
218
|
-
headers: rawRequest.headers,
|
|
219
|
-
query: rawRequest.query,
|
|
220
|
-
params: rawRequest.params,
|
|
221
|
-
body: null,
|
|
222
|
-
metadata: {},
|
|
223
|
-
requestId: this.getRequestId(rawRequest) || crypto.randomUUID(),
|
|
224
|
-
reply: this.alepha.inject(ServerReply, { lifetime: "transient" }),
|
|
225
|
-
get ip() {
|
|
226
|
-
return self.getRequestIp(rawRequest);
|
|
227
|
-
},
|
|
228
|
-
get userAgent() {
|
|
229
|
-
return self.getRequestUserAgent(rawRequest);
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
getRequestId(request) {
|
|
234
|
-
return request.headers["x-request-id"];
|
|
235
|
-
}
|
|
236
|
-
getRequestUserAgent(request) {
|
|
237
|
-
return this.userAgentParser.parse(request.headers["user-agent"]);
|
|
238
|
-
}
|
|
239
|
-
getRequestIp(request) {
|
|
240
|
-
const forwardedFor = request.headers["x-forwarded-for"];
|
|
241
|
-
if (forwardedFor) return Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor.split(",")[0].trim();
|
|
242
|
-
const xRealIP = request.headers["x-real-ip"];
|
|
243
|
-
if (xRealIP) return Array.isArray(xRealIP) ? xRealIP[0] : xRealIP;
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
//#endregion
|
|
248
|
-
//#region src/server/providers/ServerTimingProvider.ts
|
|
249
|
-
var ServerTimingProvider = class {
|
|
250
|
-
log = (0, alepha_logger.$logger)();
|
|
251
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
252
|
-
options = {
|
|
253
|
-
prefix: this.alepha.env.APP_NAME ? `${this.alepha.env.APP_NAME.toLowerCase()}-` : "",
|
|
254
|
-
disabled: this.alepha.isProduction()
|
|
255
|
-
};
|
|
256
|
-
onRequest = (0, alepha.$hook)({
|
|
257
|
-
priority: "first",
|
|
258
|
-
on: "server:onRequest",
|
|
259
|
-
handler: async ({ request }) => {
|
|
260
|
-
if (this.options.disabled) return;
|
|
261
|
-
request.metadata.timing = {};
|
|
262
|
-
request.metadata.timing[this.handlerName] = [Date.now()];
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
onResponse = (0, alepha.$hook)({
|
|
266
|
-
priority: "last",
|
|
267
|
-
on: "server:onResponse",
|
|
268
|
-
handler: async ({ request }) => {
|
|
269
|
-
if (this.options.disabled) return;
|
|
270
|
-
if (request.metadata.timing) {
|
|
271
|
-
this.setDuration(this.handlerName, request.metadata.timing);
|
|
272
|
-
let timingHeader = "";
|
|
273
|
-
for (const [name, [start, duration]] of Object.entries(request.metadata.timing)) {
|
|
274
|
-
if (typeof start !== "number" || typeof duration !== "number") {
|
|
275
|
-
this.log.warn(`Invalid timing for '${name}': [${start}, ${duration}]`);
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
const formattedName = this.options.prefix + name.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
279
|
-
timingHeader += `${formattedName};dur=${duration}, `;
|
|
280
|
-
}
|
|
281
|
-
if (request.reply.headers["Server-Timing"]) request.reply.headers["Server-Timing"] += `, ${timingHeader}`;
|
|
282
|
-
else request.reply.headers["Server-Timing"] = timingHeader;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
get handlerName() {
|
|
287
|
-
return `request`;
|
|
288
|
-
}
|
|
289
|
-
beginTiming(name) {
|
|
290
|
-
if (this.options.disabled) return;
|
|
291
|
-
const request = this.alepha.context.get("request");
|
|
292
|
-
if (!request) return;
|
|
293
|
-
request.metadata ??= {};
|
|
294
|
-
request.metadata.timing ??= {};
|
|
295
|
-
request.metadata.timing[name] = [Date.now()];
|
|
296
|
-
}
|
|
297
|
-
endTiming(name) {
|
|
298
|
-
if (this.options.disabled) return;
|
|
299
|
-
const request = this.alepha.context.get("request");
|
|
300
|
-
if (!request) return;
|
|
301
|
-
if (!request.metadata?.timing || !(name in request.metadata.timing)) {
|
|
302
|
-
this.log.warn(`No timing found for '${name}'.`);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
const start = request.metadata.timing[name]?.[0];
|
|
306
|
-
if (typeof start !== "number") {
|
|
307
|
-
this.log.warn(`Invalid timing start for '${name}': ${start}`);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
this.setDuration(name, request.metadata.timing);
|
|
311
|
-
}
|
|
312
|
-
setDuration(name, timing) {
|
|
313
|
-
timing[name] = [timing[name][0], Date.now() - timing[name][0]];
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
//#endregion
|
|
318
|
-
//#region src/server/providers/ServerRouterProvider.ts
|
|
319
|
-
/**
|
|
320
|
-
* Main router for all routes on the server side.
|
|
321
|
-
*
|
|
322
|
-
* - $route => generic route
|
|
323
|
-
* - $action => action route (for API calls)
|
|
324
|
-
* - $page => React route (for SSR)
|
|
325
|
-
*/
|
|
326
|
-
var ServerRouterProvider = class extends alepha_router.RouterProvider {
|
|
327
|
-
log = (0, alepha_logger.$logger)();
|
|
328
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
329
|
-
routes = [];
|
|
330
|
-
serverTimingProvider = (0, alepha.$inject)(ServerTimingProvider);
|
|
331
|
-
serverRequestParser = (0, alepha.$inject)(ServerRequestParser);
|
|
332
|
-
/**
|
|
333
|
-
* Get all registered routes, optionally filtered by a pattern.
|
|
334
|
-
*
|
|
335
|
-
* Pattern accept simple wildcard '*' at the end.
|
|
336
|
-
* Example: '/api/*' will match all routes starting with '/api/' but '/api/' will match only that exact route.
|
|
337
|
-
*/
|
|
338
|
-
getRoutes(pattern) {
|
|
339
|
-
if (pattern) if (pattern.endsWith("*")) {
|
|
340
|
-
const basePattern = pattern.slice(0, -1);
|
|
341
|
-
return this.routes.filter((route) => route.path.startsWith(basePattern));
|
|
342
|
-
} else return this.routes.filter((route) => route.path === pattern);
|
|
343
|
-
return this.routes;
|
|
344
|
-
}
|
|
345
|
-
createRoute(route) {
|
|
346
|
-
route.method ??= "GET";
|
|
347
|
-
route.method = route.method.toUpperCase();
|
|
348
|
-
this.routes.push(route);
|
|
349
|
-
const path = `/${route.method}/${route.path}`.replace(/\/+/g, "/");
|
|
350
|
-
const responseKind = this.getResponseType(route.schema);
|
|
351
|
-
this.log.trace(`Create route ${path}`);
|
|
352
|
-
this.push({
|
|
353
|
-
path,
|
|
354
|
-
handler: (rawRequest) => {
|
|
355
|
-
const request = this.serverRequestParser.createServerRequest(rawRequest);
|
|
356
|
-
return this.alepha.context.run(() => this.processRequest(request, route, responseKind), { context: this.getContextId(rawRequest.headers) });
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
getContextId(headers) {
|
|
361
|
-
const contextId = headers["x-request-id"] || headers["x-correlation-id"];
|
|
362
|
-
if ((0, alepha.isUUID)(contextId)) return contextId;
|
|
363
|
-
return crypto.randomUUID();
|
|
364
|
-
}
|
|
365
|
-
async processRequest(request, route, responseKind) {
|
|
366
|
-
await this.runRouteHandler(route, request, responseKind).catch((error) => {
|
|
367
|
-
return this.errorHandler(route, request, error);
|
|
368
|
-
});
|
|
369
|
-
await this.alepha.events.emit("server:onSend", {
|
|
370
|
-
request,
|
|
371
|
-
route
|
|
372
|
-
}, { catch: true });
|
|
373
|
-
const response = {
|
|
374
|
-
status: request.reply.status ?? (request.reply.body ? 200 : 204),
|
|
375
|
-
headers: request.reply.headers,
|
|
376
|
-
body: request.reply.body
|
|
377
|
-
};
|
|
378
|
-
await this.alepha.events.emit("server:onResponse", {
|
|
379
|
-
request,
|
|
380
|
-
route,
|
|
381
|
-
response
|
|
382
|
-
}, { catch: true });
|
|
383
|
-
return response;
|
|
384
|
-
}
|
|
385
|
-
async runRouteHandler(route, request, responseKind) {
|
|
386
|
-
await this.alepha.events.emit("server:onRequest", {
|
|
387
|
-
request,
|
|
388
|
-
route
|
|
389
|
-
}, { log: false });
|
|
390
|
-
if (request.reply.body || request.reply.status && request.reply.status >= 200) return;
|
|
391
|
-
this.alepha.context.set("request", request);
|
|
392
|
-
this.serverTimingProvider.beginTiming("validateRequest");
|
|
393
|
-
try {
|
|
394
|
-
this.validateRequest(route, request);
|
|
395
|
-
} finally {
|
|
396
|
-
this.serverTimingProvider.endTiming("validateRequest");
|
|
397
|
-
}
|
|
398
|
-
this.serverTimingProvider.beginTiming("runHandler");
|
|
399
|
-
try {
|
|
400
|
-
const result = await route.handler(request);
|
|
401
|
-
if (result) request.reply.body = result;
|
|
402
|
-
} finally {
|
|
403
|
-
this.serverTimingProvider.endTiming("runHandler");
|
|
404
|
-
}
|
|
405
|
-
this.serverTimingProvider.beginTiming("serializeResponse");
|
|
406
|
-
try {
|
|
407
|
-
this.serializeResponse(route, request.reply, responseKind);
|
|
408
|
-
} finally {
|
|
409
|
-
this.serverTimingProvider.endTiming("serializeResponse");
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
serializeResponse(route, reply, responseKind) {
|
|
413
|
-
if (responseKind === "json" && route.schema?.response) {
|
|
414
|
-
reply.headers["content-type"] = "application/json";
|
|
415
|
-
reply.body = this.alepha.codec.encode(route.schema.response, reply.body, { as: "string" });
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
if (responseKind === "file") {
|
|
419
|
-
if (!(0, alepha.isFileLike)(reply.body)) throw new HttpError({
|
|
420
|
-
status: 500,
|
|
421
|
-
message: "Invalid response body - not a file"
|
|
422
|
-
});
|
|
423
|
-
reply.headers["content-type"] = reply.body.type;
|
|
424
|
-
reply.headers["content-disposition"] = `attachment; filename="${reply.body.name.replaceAll("\"", "")}"`;
|
|
425
|
-
reply.body = reply.body.stream();
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
if (responseKind === "text") {
|
|
429
|
-
reply.body = String(reply.body);
|
|
430
|
-
if (reply.body.startsWith("<!DOCTYPE html>")) reply.headers["content-type"] ??= "text/html; charset=UTF-8";
|
|
431
|
-
else reply.headers["content-type"] ??= "text/plain";
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
if (reply.body == null || responseKind === "void") {
|
|
435
|
-
delete reply.headers["content-type"];
|
|
436
|
-
reply.body = void 0;
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
if (Buffer.isBuffer(reply.body)) {
|
|
440
|
-
reply.headers["content-type"] ??= "application/octet-stream";
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
if (reply.body instanceof node_stream_web.ReadableStream || reply.body instanceof node_stream.Readable) {
|
|
444
|
-
reply.headers["content-type"] ??= "application/octet-stream";
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
reply.headers["content-type"] ??= "text/plain";
|
|
448
|
-
reply.body = String(reply.body);
|
|
449
|
-
}
|
|
450
|
-
getResponseType(schema) {
|
|
451
|
-
if (schema?.response) {
|
|
452
|
-
if (alepha.t.schema.isObject(schema.response) || alepha.t.schema.isRecord(schema.response) || alepha.t.schema.isArray(schema.response)) return "json";
|
|
453
|
-
if (alepha.t.schema.isString(schema.response) || alepha.t.schema.isInteger(schema.response) || alepha.t.schema.isNumber(schema.response) || alepha.t.schema.isBoolean(schema.response)) return "text";
|
|
454
|
-
if ((0, alepha.isTypeFile)(schema.response)) return "file";
|
|
455
|
-
if (alepha.t.schema.isVoid(schema.response)) return "void";
|
|
456
|
-
}
|
|
457
|
-
return "any";
|
|
458
|
-
}
|
|
459
|
-
async errorHandler(route, request, error) {
|
|
460
|
-
request.reply.body = null;
|
|
461
|
-
await this.alepha.events.emit("server:onError", {
|
|
462
|
-
request,
|
|
463
|
-
route,
|
|
464
|
-
error
|
|
465
|
-
}, { log: false });
|
|
466
|
-
if (!request.reply.body && !request.reply.status) if (error instanceof HttpError) {
|
|
467
|
-
request.reply.status = error.status;
|
|
468
|
-
request.reply.headers["content-type"] = "application/json";
|
|
469
|
-
request.reply.body = JSON.stringify({
|
|
470
|
-
...HttpError.toJSON(error),
|
|
471
|
-
requestId: request.requestId
|
|
472
|
-
});
|
|
473
|
-
} else {
|
|
474
|
-
if ("status" in error && typeof error.status === "number" && !!errorNameByStatus[error.status]) {
|
|
475
|
-
request.reply.status = error.status;
|
|
476
|
-
request.reply.headers["content-type"] = "application/json";
|
|
477
|
-
request.reply.body = JSON.stringify({
|
|
478
|
-
status: error.status,
|
|
479
|
-
error: errorNameByStatus[error.status],
|
|
480
|
-
message: error.message,
|
|
481
|
-
requestId: request.requestId
|
|
482
|
-
});
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
request.reply.status = 500;
|
|
486
|
-
request.reply.headers["content-type"] = "application/json";
|
|
487
|
-
request.reply.body = JSON.stringify({
|
|
488
|
-
status: 500,
|
|
489
|
-
error: "InternalServerError",
|
|
490
|
-
message: error.message,
|
|
491
|
-
requestId: request.requestId
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
validateRequest(route, request) {
|
|
496
|
-
if (route.schema?.params) try {
|
|
497
|
-
request.params = this.alepha.codec.validate(route.schema.params, request.params);
|
|
498
|
-
} catch (error) {
|
|
499
|
-
throw new ValidationError("Invalid request params", error);
|
|
500
|
-
}
|
|
501
|
-
if (route.schema?.query) try {
|
|
502
|
-
const query = {};
|
|
503
|
-
for (const key in route.schema.query.properties) if (request.query[key] != null) query[key] = this.alepha.codec.decode(route.schema.query.properties[key], request.query[key]);
|
|
504
|
-
request.query = query;
|
|
505
|
-
} catch (error) {
|
|
506
|
-
throw new ValidationError("Invalid request query", error);
|
|
507
|
-
}
|
|
508
|
-
if (route.schema?.headers) try {
|
|
509
|
-
request.headers = this.alepha.codec.validate(route.schema.headers, request.headers);
|
|
510
|
-
} catch (error) {
|
|
511
|
-
throw new ValidationError("Invalid request header", error);
|
|
512
|
-
}
|
|
513
|
-
if (route.schema?.body) try {
|
|
514
|
-
request.body = this.alepha.codec.decode(route.schema.body, request.body);
|
|
515
|
-
} catch (error) {
|
|
516
|
-
throw new ValidationError("Invalid request body", error);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
//#endregion
|
|
522
|
-
//#region src/server/providers/ServerProvider.ts
|
|
523
|
-
/**
|
|
524
|
-
* Base server provider to handle incoming requests and route them.
|
|
525
|
-
*
|
|
526
|
-
* This is the default implementation for serverless environments.
|
|
527
|
-
*
|
|
528
|
-
* ServerProvider supports both Node.js HTTP requests and Web (Fetch API) requests.
|
|
529
|
-
*/
|
|
530
|
-
var ServerProvider = class {
|
|
531
|
-
log = (0, alepha_logger.$logger)();
|
|
532
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
533
|
-
dateTimeProvider = (0, alepha.$inject)(alepha_datetime.DateTimeProvider);
|
|
534
|
-
router = (0, alepha.$inject)(ServerRouterProvider);
|
|
535
|
-
internalServerErrorMessage = "Internal Server Error";
|
|
536
|
-
get hostname() {
|
|
537
|
-
return "";
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* When a Node.js HTTP request is received from outside. (Vercel, AWS Lambda, etc.)
|
|
541
|
-
*/
|
|
542
|
-
onNodeRequest = (0, alepha.$hook)({
|
|
543
|
-
on: "node:request",
|
|
544
|
-
handler: (ev) => this.handleNodeRequest(ev)
|
|
545
|
-
});
|
|
546
|
-
/**
|
|
547
|
-
* When a Web (Fetch API) request is received from outside. (Netlify, Cloudflare Workers, etc.)
|
|
548
|
-
*/
|
|
549
|
-
onWebRequest = (0, alepha.$hook)({
|
|
550
|
-
on: "web:request",
|
|
551
|
-
handler: (ev) => {
|
|
552
|
-
return this.handleWebRequest(ev);
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
/**
|
|
556
|
-
* Handle Node.js HTTP request event.
|
|
557
|
-
*
|
|
558
|
-
* Technically, we just convert Node.js request to Web Standard Request.
|
|
559
|
-
*/
|
|
560
|
-
async handleNodeRequest(nodeRequestEvent) {
|
|
561
|
-
const { req, res } = nodeRequestEvent;
|
|
562
|
-
const { route, params } = this.router.match(`/${req.method}${req.url}`);
|
|
563
|
-
if (this.isViteNotFound(req.url, route, params)) return;
|
|
564
|
-
if (!route) {
|
|
565
|
-
res.writeHead(404, { "content-type": "text/plain" });
|
|
566
|
-
res.end("Not Found");
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
const headers = req.headers ?? {};
|
|
570
|
-
const proto = headers["x-forwarded-proto"] === "https" ? "https" : "http";
|
|
571
|
-
const url = new URL(`${proto}://${headers.host}${req.url}`);
|
|
572
|
-
const query = Object.fromEntries(url.searchParams.entries());
|
|
573
|
-
const request = {
|
|
574
|
-
method: req.method?.toUpperCase() ?? "GET",
|
|
575
|
-
url,
|
|
576
|
-
headers,
|
|
577
|
-
params: params ?? {},
|
|
578
|
-
query,
|
|
579
|
-
raw: { node: nodeRequestEvent }
|
|
580
|
-
};
|
|
581
|
-
const response = await route.handler(request).catch(() => {
|
|
582
|
-
return {
|
|
583
|
-
status: 500,
|
|
584
|
-
headers: { "content-type": "text/plain" },
|
|
585
|
-
body: this.internalServerErrorMessage
|
|
586
|
-
};
|
|
587
|
-
});
|
|
588
|
-
if (!response.body) {
|
|
589
|
-
res.writeHead(response.status, response.headers).end();
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
if (typeof response.body === "string" || Buffer.isBuffer(response.body)) {
|
|
593
|
-
res.writeHead(response.status, response.headers).end(response.body);
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
if (response.body instanceof node_stream.Readable) {
|
|
597
|
-
res.writeHead(response.status, response.headers);
|
|
598
|
-
response.body.pipe(res);
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
if (response.body instanceof ReadableStream) {
|
|
602
|
-
res.writeHead(response.status, response.headers);
|
|
603
|
-
try {
|
|
604
|
-
for await (const chunk of response.body) res.write(chunk);
|
|
605
|
-
} catch (error) {
|
|
606
|
-
this.log.error("Error piping proxy response stream", error);
|
|
607
|
-
} finally {
|
|
608
|
-
res.end();
|
|
609
|
-
}
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
this.log.error("Unknown response body type:", typeof response.body);
|
|
613
|
-
res.writeHead(500, { "content-type": "text/plain" });
|
|
614
|
-
res.end(this.internalServerErrorMessage);
|
|
615
|
-
}
|
|
616
|
-
/**
|
|
617
|
-
* Handle Web (Fetch API) request event.
|
|
618
|
-
*/
|
|
619
|
-
async handleWebRequest(ev) {
|
|
620
|
-
const req = ev.req;
|
|
621
|
-
const url = new URL(req.url);
|
|
622
|
-
const { route, params } = this.router.match(`/${req.method}${url.pathname}`);
|
|
623
|
-
if (this.isViteNotFound(req.url, route, params)) return;
|
|
624
|
-
if (!route) {
|
|
625
|
-
ev.res = new Response("Not Found", {
|
|
626
|
-
status: 404,
|
|
627
|
-
headers: { "content-type": "text/plain" }
|
|
628
|
-
});
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
const headers = {};
|
|
632
|
-
req.headers.forEach((value, key) => {
|
|
633
|
-
headers[key] = value;
|
|
634
|
-
});
|
|
635
|
-
const query = Object.fromEntries(url.searchParams.entries());
|
|
636
|
-
const request = {
|
|
637
|
-
method: req.method.toUpperCase() ?? "GET",
|
|
638
|
-
url,
|
|
639
|
-
headers,
|
|
640
|
-
params: params || {},
|
|
641
|
-
query,
|
|
642
|
-
raw: { web: ev }
|
|
643
|
-
};
|
|
644
|
-
const response = await route.handler(request).catch(() => {
|
|
645
|
-
return {
|
|
646
|
-
status: 500,
|
|
647
|
-
headers: { "content-type": "text/plain" },
|
|
648
|
-
body: this.internalServerErrorMessage
|
|
649
|
-
};
|
|
650
|
-
});
|
|
651
|
-
if (!response.body) {
|
|
652
|
-
ev.res = new Response(null, {
|
|
653
|
-
status: response.status,
|
|
654
|
-
headers: response.headers
|
|
655
|
-
});
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
if (typeof response.body === "string") {
|
|
659
|
-
ev.res = new Response(response.body, {
|
|
660
|
-
status: response.status,
|
|
661
|
-
headers: response.headers
|
|
662
|
-
});
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
if (Buffer.isBuffer(response.body)) {
|
|
666
|
-
ev.res = new Response(response.body.buffer, {
|
|
667
|
-
status: response.status,
|
|
668
|
-
headers: response.headers
|
|
669
|
-
});
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
if (response.body instanceof node_stream.Readable) {
|
|
673
|
-
ev.res = new Response(node_stream.Readable.toWeb(response.body), {
|
|
674
|
-
status: response.status,
|
|
675
|
-
headers: response.headers
|
|
676
|
-
});
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
if (response.body instanceof ReadableStream) {
|
|
680
|
-
ev.res = new Response(response.body, {
|
|
681
|
-
status: response.status,
|
|
682
|
-
headers: response.headers
|
|
683
|
-
});
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
this.log.error(`Unknown response body type: ${typeof response.body}`);
|
|
687
|
-
ev.res = new Response(this.internalServerErrorMessage, {
|
|
688
|
-
status: 500,
|
|
689
|
-
headers: { "content-type": "text/plain" }
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* Helper for Vite development mode to let Vite handle (or not) 404.
|
|
694
|
-
*/
|
|
695
|
-
isViteNotFound(url, route, params) {
|
|
696
|
-
if (this.alepha.isViteDev()) {
|
|
697
|
-
if (!route) return true;
|
|
698
|
-
url = url?.split("?")[0];
|
|
699
|
-
if (!!params?.["*"] && `/${params?.["*"]}` === url) return true;
|
|
700
|
-
}
|
|
701
|
-
return false;
|
|
702
|
-
}
|
|
703
|
-
};
|
|
704
|
-
|
|
705
|
-
//#endregion
|
|
706
|
-
//#region src/server/schemas/errorSchema.ts
|
|
707
|
-
const errorSchema = alepha.t.object({
|
|
708
|
-
error: alepha.t.text({ description: "HTTP error name" }),
|
|
709
|
-
status: alepha.t.integer({ description: "HTTP status code" }),
|
|
710
|
-
message: alepha.t.text({
|
|
711
|
-
description: "Short text which describe the error",
|
|
712
|
-
size: "rich"
|
|
713
|
-
}),
|
|
714
|
-
details: alepha.t.optional(alepha.t.text({
|
|
715
|
-
description: "Detailed description of the error",
|
|
716
|
-
size: "rich"
|
|
717
|
-
})),
|
|
718
|
-
requestId: alepha.t.optional(alepha.t.text()),
|
|
719
|
-
cause: alepha.t.optional(alepha.t.object({
|
|
720
|
-
name: alepha.t.text(),
|
|
721
|
-
message: alepha.t.text({
|
|
722
|
-
description: "Cause Error message",
|
|
723
|
-
size: "rich"
|
|
724
|
-
})
|
|
725
|
-
}))
|
|
726
|
-
}, {
|
|
727
|
-
title: "HttpError",
|
|
728
|
-
description: "Generic response after a failed operation"
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
//#endregion
|
|
732
|
-
//#region src/server/services/HttpClient.ts
|
|
733
|
-
var HttpClient = class {
|
|
734
|
-
log = (0, alepha_logger.$logger)();
|
|
735
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
736
|
-
cache = (0, alepha_cache.$cache)();
|
|
737
|
-
pendingRequests = {};
|
|
738
|
-
async fetchAction(args) {
|
|
739
|
-
const route = args.action;
|
|
740
|
-
const options = args.options ?? {};
|
|
741
|
-
const config = args.config ?? {};
|
|
742
|
-
const host = args.host ?? "";
|
|
743
|
-
const request = { ...options.request };
|
|
744
|
-
const method = route.method;
|
|
745
|
-
const headers = {};
|
|
746
|
-
const url = this.url(host, route, config);
|
|
747
|
-
await this.alepha.events.emit("client:onRequest", {
|
|
748
|
-
route,
|
|
749
|
-
config,
|
|
750
|
-
options,
|
|
751
|
-
headers,
|
|
752
|
-
request
|
|
753
|
-
});
|
|
754
|
-
request.method ??= method;
|
|
755
|
-
await this.body(request, headers, route, config);
|
|
756
|
-
request.headers = {
|
|
757
|
-
...config.headers,
|
|
758
|
-
...Object.fromEntries(new Headers(request.headers).entries()),
|
|
759
|
-
...headers
|
|
760
|
-
};
|
|
761
|
-
return await this.fetch(url, {
|
|
762
|
-
...request,
|
|
763
|
-
schema: route.schema,
|
|
764
|
-
...options
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
async fetch(url, request = {}) {
|
|
768
|
-
const options = {
|
|
769
|
-
cache: request.localCache,
|
|
770
|
-
schema: request.schema?.response,
|
|
771
|
-
key: request.key
|
|
772
|
-
};
|
|
773
|
-
request.method ??= "GET";
|
|
774
|
-
this.log.trace("Request", {
|
|
775
|
-
url,
|
|
776
|
-
method: request.method,
|
|
777
|
-
body: request.body,
|
|
778
|
-
headers: request.headers,
|
|
779
|
-
options
|
|
780
|
-
});
|
|
781
|
-
const cached = await this.cache.get(url);
|
|
782
|
-
if (cached && request.method === "GET") if (cached.etag) {
|
|
783
|
-
request.headers = new Headers(request.headers);
|
|
784
|
-
if (!request.headers.has("if-none-match")) request.headers.set("if-none-match", cached.etag);
|
|
785
|
-
} else return {
|
|
786
|
-
data: cached.data,
|
|
787
|
-
status: 200,
|
|
788
|
-
statusText: "OK",
|
|
789
|
-
headers: new Headers()
|
|
790
|
-
};
|
|
791
|
-
await this.alepha.events.emit("client:beforeFetch", {
|
|
792
|
-
url,
|
|
793
|
-
options,
|
|
794
|
-
request
|
|
795
|
-
});
|
|
796
|
-
const key = options.key ?? JSON.stringify({
|
|
797
|
-
url,
|
|
798
|
-
method: request.method,
|
|
799
|
-
body: request.body
|
|
800
|
-
});
|
|
801
|
-
const existing = this.pendingRequests[key];
|
|
802
|
-
if (existing) {
|
|
803
|
-
this.log.info("Request already pending", key);
|
|
804
|
-
return existing;
|
|
805
|
-
}
|
|
806
|
-
this.pendingRequests[key] = fetch(url, request).then(async (response) => {
|
|
807
|
-
this.log.debug("Response", {
|
|
808
|
-
url,
|
|
809
|
-
status: response.status
|
|
810
|
-
});
|
|
811
|
-
const fetchResponse = {
|
|
812
|
-
data: await this.responseData(response, options),
|
|
813
|
-
status: response.status,
|
|
814
|
-
statusText: response.statusText,
|
|
815
|
-
headers: response.headers,
|
|
816
|
-
raw: response
|
|
817
|
-
};
|
|
818
|
-
if (request.method === "GET") {
|
|
819
|
-
if (options.cache) await this.cache.set(url, { data: fetchResponse.data }, typeof options.cache === "boolean" ? void 0 : options.cache);
|
|
820
|
-
else if (!this.alepha.isBrowser()) {
|
|
821
|
-
const etag = response.headers.get("etag") ?? void 0;
|
|
822
|
-
if (etag) await this.cache.set(url, {
|
|
823
|
-
data: fetchResponse.data,
|
|
824
|
-
etag
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
return fetchResponse;
|
|
829
|
-
}).finally(() => {
|
|
830
|
-
delete this.pendingRequests[key];
|
|
831
|
-
});
|
|
832
|
-
return this.pendingRequests[key];
|
|
833
|
-
}
|
|
834
|
-
url(host, action, args) {
|
|
835
|
-
let url = host;
|
|
836
|
-
if (action.prefix) url += action.prefix;
|
|
837
|
-
url += action.path;
|
|
838
|
-
url = this.pathVariables(url, action, args);
|
|
839
|
-
url = this.queryParams(url, action, args);
|
|
840
|
-
return url;
|
|
841
|
-
}
|
|
842
|
-
async body(init, headers, action, args = {}) {
|
|
843
|
-
if (typeof init.headers === "object" && "content-type" in init.headers && init.headers["content-type"] === "multipart/form-data" || isMultipart(action)) {
|
|
844
|
-
if (typeof init.headers === "object" && "content-type" in init.headers) delete init.headers["content-type"];
|
|
845
|
-
const formData = new FormData();
|
|
846
|
-
for (const [key, value] of Object.entries(args.body ?? {})) {
|
|
847
|
-
if (typeof value === "string") {
|
|
848
|
-
formData.append(key, value);
|
|
849
|
-
continue;
|
|
850
|
-
}
|
|
851
|
-
if (value instanceof Blob) {
|
|
852
|
-
formData.append(key, value);
|
|
853
|
-
continue;
|
|
854
|
-
}
|
|
855
|
-
if ((0, alepha.isFileLike)(value)) formData.append(key, new File([await value.arrayBuffer()], value.name, { type: value.type }));
|
|
856
|
-
}
|
|
857
|
-
init.body = formData;
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
if (!init.body && action.schema?.body) {
|
|
861
|
-
headers["content-type"] = "application/json";
|
|
862
|
-
init.body = this.alepha.codec.encode(action.schema?.body, args.body, { as: "string" });
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
async responseData(response, options) {
|
|
866
|
-
if (response.status === 304) {
|
|
867
|
-
let cacheKey = response.url;
|
|
868
|
-
if (typeof window !== "undefined") cacheKey = cacheKey.replace(window.location.origin, "");
|
|
869
|
-
const cached = await this.cache.get(cacheKey);
|
|
870
|
-
if (cached) return cached.data;
|
|
871
|
-
return "";
|
|
872
|
-
}
|
|
873
|
-
if (response.status === 204) return;
|
|
874
|
-
if (this.isMaybeFile(response)) return this.createFileLike(response);
|
|
875
|
-
if (response.headers.get("Content-Type")?.startsWith("text/")) return await response.text();
|
|
876
|
-
if (response.headers.get("Content-Type") === "application/json") {
|
|
877
|
-
const json = await response.json();
|
|
878
|
-
if (response.status >= 400) {
|
|
879
|
-
const error = new HttpError(this.alepha.codec.decode(errorSchema, json));
|
|
880
|
-
await this.alepha.events.emit("client:onError", { error });
|
|
881
|
-
throw error;
|
|
882
|
-
}
|
|
883
|
-
if (options.schema) return this.alepha.codec.decode(options.schema, json);
|
|
884
|
-
return json;
|
|
885
|
-
}
|
|
886
|
-
if (response.status >= 400) {
|
|
887
|
-
const error = new HttpError({
|
|
888
|
-
status: response.status,
|
|
889
|
-
message: `An error occurred while fetching the resource. (${response.statusText})`
|
|
890
|
-
});
|
|
891
|
-
await this.alepha.events.emit("client:onError", { error });
|
|
892
|
-
throw error;
|
|
893
|
-
}
|
|
894
|
-
return response;
|
|
895
|
-
}
|
|
896
|
-
isMaybeFile(response) {
|
|
897
|
-
const contentType = response.headers.get("Content-Type");
|
|
898
|
-
if (!contentType) return false;
|
|
899
|
-
if (response.headers.get("Content-Disposition")?.includes("attachment")) return true;
|
|
900
|
-
return contentType.startsWith("application/octet-stream") || contentType.startsWith("application/pdf") || contentType.startsWith("application/zip") || contentType.startsWith("image/") || contentType.startsWith("video/") || contentType.startsWith("audio/");
|
|
901
|
-
}
|
|
902
|
-
createFileLike(response, defaultFileName = "") {
|
|
903
|
-
const match = (response.headers.get("Content-Disposition") ?? "").match(/filename="(.+)"/);
|
|
904
|
-
return {
|
|
905
|
-
name: match?.[1] ? match[1] : defaultFileName,
|
|
906
|
-
type: response.headers.get("Content-Type") ?? "application/octet-stream",
|
|
907
|
-
size: Number(response.headers.get("Content-Length") ?? 0),
|
|
908
|
-
lastModified: Date.now(),
|
|
909
|
-
stream: () => {
|
|
910
|
-
throw new Error("Not implemented");
|
|
911
|
-
},
|
|
912
|
-
arrayBuffer: async () => {
|
|
913
|
-
return await response.arrayBuffer();
|
|
914
|
-
},
|
|
915
|
-
text: async () => {
|
|
916
|
-
return await response.text();
|
|
917
|
-
}
|
|
918
|
-
};
|
|
919
|
-
}
|
|
920
|
-
pathVariables(url, action, args = {}) {
|
|
921
|
-
if (typeof args.params === "object") {
|
|
922
|
-
const params = action.schema?.params ? this.alepha.codec.decode(action.schema.params, args.params) : args.params;
|
|
923
|
-
for (const key of Object.keys(params)) {
|
|
924
|
-
url = url.replace(`:${key}`, params[key]);
|
|
925
|
-
url = url.replace(`{${key}}`, params[key]);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
return url;
|
|
929
|
-
}
|
|
930
|
-
queryParams(url, action, args = {}) {
|
|
931
|
-
if (typeof args.query === "object") {
|
|
932
|
-
const query = action.schema?.query ? this.alepha.codec.decode(action.schema.query, args.query ?? {}) : args.query;
|
|
933
|
-
for (const key of Object.keys(query)) {
|
|
934
|
-
if (query[key] === void 0) delete query[key];
|
|
935
|
-
if (typeof query[key] === "object") query[key] = JSON.stringify(query[key]);
|
|
936
|
-
}
|
|
937
|
-
return `${url}?${new URLSearchParams(query).toString()}`;
|
|
938
|
-
}
|
|
939
|
-
return url;
|
|
940
|
-
}
|
|
941
|
-
};
|
|
942
|
-
|
|
943
|
-
//#endregion
|
|
944
|
-
//#region src/server/descriptors/$action.ts
|
|
945
|
-
/**
|
|
946
|
-
* Creates a server action descriptor for defining type-safe HTTP endpoints.
|
|
947
|
-
*
|
|
948
|
-
* Server actions are the core building blocks for REST APIs in Alepha, providing declarative
|
|
949
|
-
* HTTP endpoints with full type safety, automatic validation, and OpenAPI documentation.
|
|
950
|
-
*
|
|
951
|
-
* **Key Features**
|
|
952
|
-
* - Full TypeScript inference for request/response types
|
|
953
|
-
* - Automatic schema validation using TypeBox
|
|
954
|
-
* - Convention-based URL generation with customizable paths
|
|
955
|
-
* - Direct invocation (`run()`) or HTTP requests (`fetch()`)
|
|
956
|
-
* - Built-in authentication and authorization support
|
|
957
|
-
* - Automatic content-type handling (JSON, form-data, plain text)
|
|
958
|
-
*
|
|
959
|
-
* **URL Generation**
|
|
960
|
-
* Actions are prefixed with `/api` by default (configurable via `SERVER_API_PREFIX`).
|
|
961
|
-
* HTTP method defaults to GET, or POST if body schema is provided.
|
|
962
|
-
*
|
|
963
|
-
* **Common Use Cases**
|
|
964
|
-
* - CRUD operations with type safety
|
|
965
|
-
* - File upload and download endpoints
|
|
966
|
-
* - Microservice communication
|
|
967
|
-
*
|
|
968
|
-
* @example
|
|
969
|
-
* ```ts
|
|
970
|
-
* class UserController {
|
|
971
|
-
* getUsers = $action({
|
|
972
|
-
* path: "/users",
|
|
973
|
-
* schema: {
|
|
974
|
-
* query: t.object({
|
|
975
|
-
* page: t.optional(t.number({ default: 1 })),
|
|
976
|
-
* limit: t.optional(t.number({ default: 10 }))
|
|
977
|
-
* }),
|
|
978
|
-
* response: t.object({
|
|
979
|
-
* users: t.array(t.object({
|
|
980
|
-
* id: t.text(),
|
|
981
|
-
* name: t.text(),
|
|
982
|
-
* email: t.text()
|
|
983
|
-
* })),
|
|
984
|
-
* total: t.number()
|
|
985
|
-
* })
|
|
986
|
-
* },
|
|
987
|
-
* handler: async ({ query }) => {
|
|
988
|
-
* const users = await this.userService.findUsers(query);
|
|
989
|
-
* return { users: users.items, total: users.total };
|
|
990
|
-
* }
|
|
991
|
-
* });
|
|
992
|
-
*
|
|
993
|
-
* createUser = $action({
|
|
994
|
-
* method: "POST",
|
|
995
|
-
* path: "/users",
|
|
996
|
-
* schema: {
|
|
997
|
-
* body: t.object({
|
|
998
|
-
* name: t.text(),
|
|
999
|
-
* email: t.text({ format: "email" })
|
|
1000
|
-
* }),
|
|
1001
|
-
* response: t.object({ id: t.text(), name: t.text() })
|
|
1002
|
-
* },
|
|
1003
|
-
* handler: async ({ body }) => {
|
|
1004
|
-
* return await this.userService.create(body);
|
|
1005
|
-
* }
|
|
1006
|
-
* });
|
|
1007
|
-
* }
|
|
1008
|
-
* ```
|
|
1009
|
-
*/
|
|
1010
|
-
const $action = (options) => {
|
|
1011
|
-
const instance = (0, alepha.createDescriptor)(ActionDescriptor, options);
|
|
1012
|
-
const fn = (config, options$1) => {
|
|
1013
|
-
return instance.run(config, options$1);
|
|
1014
|
-
};
|
|
1015
|
-
Object.defineProperty(fn, "name", { get() {
|
|
1016
|
-
return instance.options.name || instance.config.propertyKey;
|
|
1017
|
-
} });
|
|
1018
|
-
return Object.setPrototypeOf(fn, instance);
|
|
1019
|
-
};
|
|
1020
|
-
const envSchema$3 = alepha.t.object({ SERVER_API_PREFIX: alepha.t.text({
|
|
1021
|
-
description: "Prefix for all API routes (e.g. $action).",
|
|
1022
|
-
default: "/api"
|
|
1023
|
-
}) });
|
|
1024
|
-
var ActionDescriptor = class extends alepha.Descriptor {
|
|
1025
|
-
log = (0, alepha_logger.$logger)();
|
|
1026
|
-
env = (0, alepha.$env)(envSchema$3);
|
|
1027
|
-
httpClient = (0, alepha.$inject)(HttpClient);
|
|
1028
|
-
serverProvider = (0, alepha.$inject)(ServerProvider);
|
|
1029
|
-
serverRouterProvider = (0, alepha.$inject)(ServerRouterProvider);
|
|
1030
|
-
onInit() {
|
|
1031
|
-
if (this.options.disabled) {
|
|
1032
|
-
this.log.debug(`Action '${this.name}' is disabled. It won't be available in the API.`);
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
this.serverRouterProvider.createRoute(this.route);
|
|
1036
|
-
}
|
|
1037
|
-
get prefix() {
|
|
1038
|
-
return this.env.SERVER_API_PREFIX;
|
|
1039
|
-
}
|
|
1040
|
-
get route() {
|
|
1041
|
-
return {
|
|
1042
|
-
...this.options,
|
|
1043
|
-
method: this.method,
|
|
1044
|
-
path: `${this.prefix}${this.path}`
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
/**
|
|
1048
|
-
* Returns the name of the action.
|
|
1049
|
-
*/
|
|
1050
|
-
get name() {
|
|
1051
|
-
return this.options.name || this.config.propertyKey;
|
|
1052
|
-
}
|
|
1053
|
-
/**
|
|
1054
|
-
* Returns the group of the action. (e.g. "orders", "admin", etc.)
|
|
1055
|
-
*/
|
|
1056
|
-
get group() {
|
|
1057
|
-
return this.options.group || this.config.service.name;
|
|
1058
|
-
}
|
|
1059
|
-
/**
|
|
1060
|
-
* Returns the HTTP method of the action.
|
|
1061
|
-
*/
|
|
1062
|
-
get method() {
|
|
1063
|
-
return this.options.method || (this.options.schema?.body ? "POST" : "GET");
|
|
1064
|
-
}
|
|
1065
|
-
/**
|
|
1066
|
-
* Returns the path of the action.
|
|
1067
|
-
*
|
|
1068
|
-
* Path is prefixed by `/api` by default.
|
|
1069
|
-
*/
|
|
1070
|
-
get path() {
|
|
1071
|
-
if (this.options.path) return this.options.path;
|
|
1072
|
-
let path = `/${this.name}`;
|
|
1073
|
-
if (this.options.schema?.params) for (const [key] of Object.entries(this.options.schema.params.properties)) path += `/:${key}`;
|
|
1074
|
-
return path;
|
|
1075
|
-
}
|
|
1076
|
-
get schema() {
|
|
1077
|
-
return this.options.schema;
|
|
1078
|
-
}
|
|
1079
|
-
getBodyContentType() {
|
|
1080
|
-
if (this.options.schema?.body) {
|
|
1081
|
-
if (isMultipart(this.options)) return "multipart/form-data";
|
|
1082
|
-
if (alepha.t.schema.isString(this.options.schema.body)) return "text/plain";
|
|
1083
|
-
if (alepha.t.schema.isObject(this.options.schema.body) || alepha.t.schema.isArray(this.options.schema.body) || alepha.t.schema.isRecord(this.options.schema.body)) return "application/json";
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
/**
|
|
1087
|
-
* Call the action handler directly.
|
|
1088
|
-
* There is no HTTP layer involved.
|
|
1089
|
-
*/
|
|
1090
|
-
async run(config, options = {}) {
|
|
1091
|
-
const handler = this.options.handler;
|
|
1092
|
-
const { body, params = {}, query = {}, headers = {} } = config ?? {};
|
|
1093
|
-
const reply = new ServerReply();
|
|
1094
|
-
const serverActionRequest = {
|
|
1095
|
-
method: this.method,
|
|
1096
|
-
url: new URL(`http://localhost${this.path ?? ""}`),
|
|
1097
|
-
body,
|
|
1098
|
-
params,
|
|
1099
|
-
query,
|
|
1100
|
-
headers,
|
|
1101
|
-
reply,
|
|
1102
|
-
metadata: {}
|
|
1103
|
-
};
|
|
1104
|
-
await this.alepha.events.emit("action:onRequest", {
|
|
1105
|
-
action: this,
|
|
1106
|
-
request: serverActionRequest,
|
|
1107
|
-
options
|
|
1108
|
-
});
|
|
1109
|
-
if (serverActionRequest.reply?.body) return serverActionRequest.reply.body;
|
|
1110
|
-
if (serverActionRequest.query && this.options.schema?.query) serverActionRequest.query = this.alepha.codec.encode(this.options.schema.query, serverActionRequest.query);
|
|
1111
|
-
if (serverActionRequest.headers && this.options.schema?.headers) serverActionRequest.headers = this.alepha.codec.encode(this.options.schema.headers, serverActionRequest.headers);
|
|
1112
|
-
if (serverActionRequest.body && this.options.schema?.body) serverActionRequest.body = this.alepha.codec.encode(this.options.schema.body, serverActionRequest.body);
|
|
1113
|
-
if (serverActionRequest.params && this.options.schema?.params) serverActionRequest.params = this.alepha.codec.encode(this.options.schema.params, serverActionRequest.params);
|
|
1114
|
-
this.serverRouterProvider.validateRequest(this.options, serverActionRequest);
|
|
1115
|
-
let response = await handler(serverActionRequest);
|
|
1116
|
-
if (this.options.schema?.response && !(0, alepha.isTypeFile)(this.options.schema.response)) response = this.alepha.codec.validate(this.options.schema.response, response);
|
|
1117
|
-
await this.alepha.events.emit("action:onResponse", {
|
|
1118
|
-
action: this,
|
|
1119
|
-
request: serverActionRequest,
|
|
1120
|
-
options,
|
|
1121
|
-
response
|
|
1122
|
-
});
|
|
1123
|
-
return response;
|
|
1124
|
-
}
|
|
1125
|
-
/**
|
|
1126
|
-
* Works like `run`, but always fetches (http request) the route.
|
|
1127
|
-
*/
|
|
1128
|
-
fetch(config, options) {
|
|
1129
|
-
return this.httpClient.fetchAction({
|
|
1130
|
-
host: this.serverProvider.hostname,
|
|
1131
|
-
action: this,
|
|
1132
|
-
config,
|
|
1133
|
-
options
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
};
|
|
1137
|
-
$action[alepha.KIND] = ActionDescriptor;
|
|
1138
|
-
|
|
1139
|
-
//#endregion
|
|
1140
|
-
//#region src/server/descriptors/$route.ts
|
|
1141
|
-
/**
|
|
1142
|
-
* Create a basic endpoint.
|
|
1143
|
-
*
|
|
1144
|
-
* It's a low level descriptor. You probably want to use `$action` instead.
|
|
1145
|
-
*
|
|
1146
|
-
* @see {@link $action}
|
|
1147
|
-
* @see {@link $page}
|
|
1148
|
-
*/
|
|
1149
|
-
const $route = (options) => {
|
|
1150
|
-
return (0, alepha.createDescriptor)(RouteDescriptor, options);
|
|
1151
|
-
};
|
|
1152
|
-
var RouteDescriptor = class extends alepha.Descriptor {
|
|
1153
|
-
serverRouterProvider = (0, alepha.$inject)(ServerRouterProvider);
|
|
1154
|
-
onInit() {
|
|
1155
|
-
this.serverRouterProvider.createRoute(this.options);
|
|
1156
|
-
}
|
|
1157
|
-
};
|
|
1158
|
-
$route[alepha.KIND] = RouteDescriptor;
|
|
1159
|
-
|
|
1160
|
-
//#endregion
|
|
1161
|
-
//#region src/server/providers/BunHttpServerProvider.ts
|
|
1162
|
-
const envSchema$2 = alepha.t.object({
|
|
1163
|
-
SERVER_PORT: alepha.t.integer({
|
|
1164
|
-
default: 3e3,
|
|
1165
|
-
min: 0,
|
|
1166
|
-
max: 65535,
|
|
1167
|
-
description: "Set 0 to listen on a random port."
|
|
1168
|
-
}),
|
|
1169
|
-
SERVER_HOST: alepha.t.text({
|
|
1170
|
-
default: "localhost",
|
|
1171
|
-
description: "Set 0.0.0.0 to listen on all interfaces."
|
|
1172
|
-
})
|
|
1173
|
-
});
|
|
1174
|
-
var BunHttpServerProvider = class extends ServerProvider {
|
|
1175
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
1176
|
-
dateTimeProvider = (0, alepha.$inject)(alepha_datetime.DateTimeProvider);
|
|
1177
|
-
log = (0, alepha_logger.$logger)();
|
|
1178
|
-
env = (0, alepha.$env)(envSchema$2);
|
|
1179
|
-
router = (0, alepha.$inject)(ServerRouterProvider);
|
|
1180
|
-
bunServer;
|
|
1181
|
-
get hostname() {
|
|
1182
|
-
if (this.bunServer) return `http://${this.bunServer.hostname}:${this.bunServer.port}`;
|
|
1183
|
-
return `http://${this.env.SERVER_HOST}:${this.env.SERVER_PORT}`;
|
|
1184
|
-
}
|
|
1185
|
-
start = (0, alepha.$hook)({
|
|
1186
|
-
on: "start",
|
|
1187
|
-
handler: async () => {
|
|
1188
|
-
await this.listen();
|
|
1189
|
-
}
|
|
1190
|
-
});
|
|
1191
|
-
stop = (0, alepha.$hook)({
|
|
1192
|
-
on: "stop",
|
|
1193
|
-
handler: async () => {
|
|
1194
|
-
if (this.alepha.isProduction()) {
|
|
1195
|
-
await this.close();
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
this.close().catch(() => {});
|
|
1199
|
-
}
|
|
1200
|
-
});
|
|
1201
|
-
async listen() {
|
|
1202
|
-
let port = this.env.SERVER_PORT;
|
|
1203
|
-
if (this.alepha.isTest() && port === 3e3) port = 0;
|
|
1204
|
-
try {
|
|
1205
|
-
this.bunServer = Bun.serve({
|
|
1206
|
-
port,
|
|
1207
|
-
hostname: this.env.SERVER_HOST,
|
|
1208
|
-
fetch: async (request) => {
|
|
1209
|
-
this.log.trace(`Incoming Bun request -> ${request.url}`);
|
|
1210
|
-
const webRequestEvent = {
|
|
1211
|
-
req: request,
|
|
1212
|
-
res: void 0
|
|
1213
|
-
};
|
|
1214
|
-
try {
|
|
1215
|
-
await this.handleWebRequest(webRequestEvent);
|
|
1216
|
-
if (!webRequestEvent.res) return new Response("Internal Server Error", {
|
|
1217
|
-
status: 500,
|
|
1218
|
-
headers: { "content-type": "text/plain" }
|
|
1219
|
-
});
|
|
1220
|
-
return webRequestEvent.res;
|
|
1221
|
-
} catch (err) {
|
|
1222
|
-
this.log.error("Error handling request", err);
|
|
1223
|
-
return new Response("Internal Server Error", {
|
|
1224
|
-
status: 500,
|
|
1225
|
-
headers: { "content-type": "text/plain" }
|
|
1226
|
-
});
|
|
1227
|
-
}
|
|
1228
|
-
},
|
|
1229
|
-
error: (error) => {
|
|
1230
|
-
this.log.error("Bun server error", error);
|
|
1231
|
-
return new Response("Internal Server Error", {
|
|
1232
|
-
status: 500,
|
|
1233
|
-
headers: { "content-type": "text/plain" }
|
|
1234
|
-
});
|
|
1235
|
-
}
|
|
1236
|
-
});
|
|
1237
|
-
this.log.info(`Server listening on ${this.hostname}`);
|
|
1238
|
-
} catch (err) {
|
|
1239
|
-
this.log.error("Failed to start Bun server", err);
|
|
1240
|
-
throw err;
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
async close() {
|
|
1244
|
-
if (!this.bunServer) return;
|
|
1245
|
-
try {
|
|
1246
|
-
const stopPromise = this.bunServer.stop();
|
|
1247
|
-
await Promise.race([this.dateTimeProvider.wait(1e4), stopPromise]);
|
|
1248
|
-
this.bunServer = void 0;
|
|
1249
|
-
this.log.info("Server closed");
|
|
1250
|
-
} catch (err) {
|
|
1251
|
-
this.log.error("Error closing Bun server", err);
|
|
1252
|
-
throw err;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
};
|
|
1256
|
-
|
|
1257
|
-
//#endregion
|
|
1258
|
-
//#region src/server/providers/NodeHttpServerProvider.ts
|
|
1259
|
-
const envSchema$1 = alepha.t.object({
|
|
1260
|
-
SERVER_PORT: alepha.t.integer({
|
|
1261
|
-
default: 3e3,
|
|
1262
|
-
min: 0,
|
|
1263
|
-
max: 65535,
|
|
1264
|
-
description: "Set 0 to listen on a random port."
|
|
1265
|
-
}),
|
|
1266
|
-
SERVER_HOST: alepha.t.text({
|
|
1267
|
-
default: "localhost",
|
|
1268
|
-
description: "Set 0.0.0.0 to listen on all interfaces."
|
|
1269
|
-
})
|
|
1270
|
-
});
|
|
1271
|
-
var NodeHttpServerProvider = class extends ServerProvider {
|
|
1272
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
1273
|
-
dateTimeProvider = (0, alepha.$inject)(alepha_datetime.DateTimeProvider);
|
|
1274
|
-
log = (0, alepha_logger.$logger)();
|
|
1275
|
-
env = (0, alepha.$env)(envSchema$1);
|
|
1276
|
-
router = (0, alepha.$inject)(ServerRouterProvider);
|
|
1277
|
-
get hostname() {
|
|
1278
|
-
if (this.server.listening) {
|
|
1279
|
-
const address = this.server.address();
|
|
1280
|
-
if (typeof address === "object" && address !== null) return `http://${this.env.SERVER_HOST}:${address.port}`;
|
|
1281
|
-
}
|
|
1282
|
-
return `http://${this.env.SERVER_HOST}:${this.env.SERVER_PORT}`;
|
|
1283
|
-
}
|
|
1284
|
-
server = this.createHttpServer((req, res) => {
|
|
1285
|
-
this.log.trace(`Incoming Node.js message -> ${req.url}`);
|
|
1286
|
-
this.handleNodeRequest({
|
|
1287
|
-
req,
|
|
1288
|
-
res
|
|
1289
|
-
}).catch((err) => {
|
|
1290
|
-
this.log.error("Error handling request", err);
|
|
1291
|
-
res.statusCode = 500;
|
|
1292
|
-
res.end("Internal Server Error");
|
|
1293
|
-
});
|
|
1294
|
-
});
|
|
1295
|
-
start = (0, alepha.$hook)({
|
|
1296
|
-
on: "start",
|
|
1297
|
-
handler: async () => {
|
|
1298
|
-
await this.listen();
|
|
1299
|
-
this.alepha.state.set("alepha.node.server", this.server);
|
|
1300
|
-
}
|
|
1301
|
-
});
|
|
1302
|
-
createHttpServer(func) {
|
|
1303
|
-
return (0, node_http.createServer)({ keepAlive: true }, func);
|
|
1304
|
-
}
|
|
1305
|
-
stop = (0, alepha.$hook)({
|
|
1306
|
-
on: "stop",
|
|
1307
|
-
handler: async () => {
|
|
1308
|
-
if (this.alepha.isProduction()) {
|
|
1309
|
-
await this.close();
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
this.close().catch(() => {});
|
|
1313
|
-
}
|
|
1314
|
-
});
|
|
1315
|
-
async listen() {
|
|
1316
|
-
let port = this.env.SERVER_PORT;
|
|
1317
|
-
if (this.alepha.isTest() && port === 3e3) port = 0;
|
|
1318
|
-
await new Promise((resolve, reject) => {
|
|
1319
|
-
this.server?.listen(port, this.env.SERVER_HOST, () => {
|
|
1320
|
-
this.log.info(`Server listening on ${this.hostname}`);
|
|
1321
|
-
resolve();
|
|
1322
|
-
});
|
|
1323
|
-
this.server?.on("error", (err) => {
|
|
1324
|
-
reject(err);
|
|
1325
|
-
});
|
|
1326
|
-
});
|
|
1327
|
-
}
|
|
1328
|
-
async close() {
|
|
1329
|
-
const promise = new Promise((resolve, reject) => {
|
|
1330
|
-
this.server?.close((err) => {
|
|
1331
|
-
if (err) reject(err);
|
|
1332
|
-
else resolve();
|
|
1333
|
-
});
|
|
1334
|
-
});
|
|
1335
|
-
await Promise.race([this.dateTimeProvider.wait(2e3), promise]);
|
|
1336
|
-
this.log.info("Server closed");
|
|
1337
|
-
}
|
|
1338
|
-
};
|
|
1339
|
-
|
|
1340
|
-
//#endregion
|
|
1341
|
-
//#region src/server/providers/ServerBodyParserProvider.ts
|
|
1342
|
-
const envSchema = alepha.t.object({
|
|
1343
|
-
SERVER_BODY_PARSER_INFLATE: alepha.t.boolean({
|
|
1344
|
-
default: true,
|
|
1345
|
-
description: "Enable decompression of request body."
|
|
1346
|
-
}),
|
|
1347
|
-
SERVER_BODY_PARSER_LIMIT: alepha.t.integer({
|
|
1348
|
-
default: 1e5,
|
|
1349
|
-
min: 0,
|
|
1350
|
-
description: "Maximum size of request body in bytes."
|
|
1351
|
-
})
|
|
1352
|
-
});
|
|
1353
|
-
var ServerBodyParserProvider = class {
|
|
1354
|
-
env = (0, alepha.$env)(envSchema);
|
|
1355
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
1356
|
-
log = (0, alepha_logger.$logger)();
|
|
1357
|
-
onRequest = (0, alepha.$hook)({
|
|
1358
|
-
on: "server:onRequest",
|
|
1359
|
-
handler: async ({ route, request }) => {
|
|
1360
|
-
if (request.body) return;
|
|
1361
|
-
let stream;
|
|
1362
|
-
if (request.raw.web?.req.body) stream = request.raw.web.req.body;
|
|
1363
|
-
else if (request.raw.node?.req) stream = node_stream_web.ReadableStream.from(request.raw.node.req);
|
|
1364
|
-
if (!stream) return;
|
|
1365
|
-
if (route.schema?.body) try {
|
|
1366
|
-
const body = await this.parse(stream, request.headers);
|
|
1367
|
-
if (body) request.body = body;
|
|
1368
|
-
} catch (error) {
|
|
1369
|
-
if (error instanceof HttpError) throw error;
|
|
1370
|
-
throw new HttpError({
|
|
1371
|
-
status: 400,
|
|
1372
|
-
message: "Failed to parse request body"
|
|
1373
|
-
}, error);
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
});
|
|
1377
|
-
async parse(stream, headers) {
|
|
1378
|
-
const contentType = headers["content-type"];
|
|
1379
|
-
const contentEncoding = headers["content-encoding"];
|
|
1380
|
-
if (!contentType) return void 0;
|
|
1381
|
-
if (contentType.startsWith("application/json")) return this.parseJson(stream, contentEncoding);
|
|
1382
|
-
if (contentType.startsWith("text/plain")) return this.parseText(stream, contentEncoding);
|
|
1383
|
-
if (contentType.startsWith("application/x-www-form-urlencoded")) return this.parseUrlEncoded(stream, contentEncoding);
|
|
1384
|
-
}
|
|
1385
|
-
async parseText(stream, contentEncoding) {
|
|
1386
|
-
const buffer = await this.streamToBuffer(stream);
|
|
1387
|
-
return (await this.maybeDecompress(buffer, contentEncoding)).toString("utf-8");
|
|
1388
|
-
}
|
|
1389
|
-
async parseUrlEncoded(stream, contentEncoding) {
|
|
1390
|
-
const text = await this.parseText(stream, contentEncoding);
|
|
1391
|
-
const params = new URLSearchParams(text);
|
|
1392
|
-
const result = {};
|
|
1393
|
-
for (const [key, value] of params.entries()) result[key] = value;
|
|
1394
|
-
return result;
|
|
1395
|
-
}
|
|
1396
|
-
async parseJson(stream, contentEncoding) {
|
|
1397
|
-
const text = await this.parseText(stream, contentEncoding);
|
|
1398
|
-
return JSON.parse(text);
|
|
1399
|
-
}
|
|
1400
|
-
async maybeDecompress(buffer, encoding) {
|
|
1401
|
-
if (!this.env.SERVER_BODY_PARSER_INFLATE && encoding) throw new HttpError({
|
|
1402
|
-
status: 415,
|
|
1403
|
-
message: `Content-Encoding ${encoding} not allowed`
|
|
1404
|
-
});
|
|
1405
|
-
switch (encoding) {
|
|
1406
|
-
case "gzip": return new Promise((res, rej) => (0, node_zlib.createGunzip)().end(buffer, () => {}).on("data", res).on("error", rej));
|
|
1407
|
-
case "deflate": return new Promise((res, rej) => (0, node_zlib.createInflate)().end(buffer, () => {}).on("data", res).on("error", rej));
|
|
1408
|
-
case "br": return new Promise((res, rej) => (0, node_zlib.createBrotliDecompress)().end(buffer, () => {}).on("data", res).on("error", rej));
|
|
1409
|
-
case void 0:
|
|
1410
|
-
case "identity": return buffer;
|
|
1411
|
-
default: throw new Error(`Unsupported Content-Encoding: ${encoding}`);
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
/**
|
|
1415
|
-
* Convert Web ReadableStream to Buffer, with a size limit.
|
|
1416
|
-
*
|
|
1417
|
-
* TODO: move to alepha/file FileUtils
|
|
1418
|
-
*/
|
|
1419
|
-
async streamToBuffer(stream) {
|
|
1420
|
-
const chunks = [];
|
|
1421
|
-
let totalLength = 0;
|
|
1422
|
-
const reader = stream.getReader();
|
|
1423
|
-
try {
|
|
1424
|
-
while (true) {
|
|
1425
|
-
const { done, value } = await reader.read();
|
|
1426
|
-
if (done) break;
|
|
1427
|
-
if (value) {
|
|
1428
|
-
totalLength += value.length;
|
|
1429
|
-
if (totalLength > this.env.SERVER_BODY_PARSER_LIMIT) {
|
|
1430
|
-
this.log.error(`Body size limit exceeded: ${totalLength} > ${this.env.SERVER_BODY_PARSER_LIMIT}`);
|
|
1431
|
-
await reader.cancel();
|
|
1432
|
-
throw new HttpError({
|
|
1433
|
-
status: 413,
|
|
1434
|
-
message: `Request body size limit exceeded`
|
|
1435
|
-
});
|
|
1436
|
-
}
|
|
1437
|
-
chunks.push(value);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
const combinedLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
1441
|
-
const combined = new Uint8Array(combinedLength);
|
|
1442
|
-
let offset = 0;
|
|
1443
|
-
for (const chunk of chunks) {
|
|
1444
|
-
combined.set(chunk, offset);
|
|
1445
|
-
offset += chunk.length;
|
|
1446
|
-
}
|
|
1447
|
-
return Buffer.from(combined);
|
|
1448
|
-
} catch (error) {
|
|
1449
|
-
reader.releaseLock();
|
|
1450
|
-
throw error;
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
};
|
|
1454
|
-
|
|
1455
|
-
//#endregion
|
|
1456
|
-
//#region src/server/providers/ServerLoggerProvider.ts
|
|
1457
|
-
var ServerLoggerProvider = class {
|
|
1458
|
-
log = (0, alepha_logger.$logger)();
|
|
1459
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
1460
|
-
onRequest = (0, alepha.$hook)({
|
|
1461
|
-
on: "server:onRequest",
|
|
1462
|
-
priority: "first",
|
|
1463
|
-
handler: ({ route, request }) => {
|
|
1464
|
-
if (!route.silent) {
|
|
1465
|
-
request.metadata.now = Date.now();
|
|
1466
|
-
const data = {
|
|
1467
|
-
method: request.method,
|
|
1468
|
-
path: request.url.pathname
|
|
1469
|
-
};
|
|
1470
|
-
if (this.alepha.isProduction()) {
|
|
1471
|
-
data.agent = request.headers["user-agent"];
|
|
1472
|
-
const ip = request.ip;
|
|
1473
|
-
if (ip) data.ip = ip;
|
|
1474
|
-
}
|
|
1475
|
-
this.log.info("Incoming request", data);
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
});
|
|
1479
|
-
onError = (0, alepha.$hook)({
|
|
1480
|
-
on: "server:onError",
|
|
1481
|
-
priority: "last",
|
|
1482
|
-
handler: ({ error }) => {
|
|
1483
|
-
this.log.error("Request has failed", error);
|
|
1484
|
-
}
|
|
1485
|
-
});
|
|
1486
|
-
onResponse = (0, alepha.$hook)({
|
|
1487
|
-
on: "server:onResponse",
|
|
1488
|
-
priority: "last",
|
|
1489
|
-
handler: ({ route, request, response }) => {
|
|
1490
|
-
if (!route.silent) {
|
|
1491
|
-
const ms = Date.now() - request.metadata.now;
|
|
1492
|
-
this.log.info("Request completed", {
|
|
1493
|
-
status: response.status,
|
|
1494
|
-
ms
|
|
1495
|
-
});
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
});
|
|
1499
|
-
};
|
|
1500
|
-
|
|
1501
|
-
//#endregion
|
|
1502
|
-
//#region src/server/providers/ServerNotReadyProvider.ts
|
|
1503
|
-
/**
|
|
1504
|
-
* On every request, this provider checks if the server is ready.
|
|
1505
|
-
*
|
|
1506
|
-
* If the server is not ready, it responds with a 503 status code and a message indicating that the server is not ready yet.
|
|
1507
|
-
*
|
|
1508
|
-
* The response also includes a `Retry-After` header indicating that the client should retry after 5 seconds.
|
|
1509
|
-
*/
|
|
1510
|
-
var ServerNotReadyProvider = class {
|
|
1511
|
-
alepha = (0, alepha.$inject)(alepha.Alepha);
|
|
1512
|
-
onRequest = (0, alepha.$hook)({
|
|
1513
|
-
on: "server:onRequest",
|
|
1514
|
-
priority: "first",
|
|
1515
|
-
handler: ({ request: { reply } }) => {
|
|
1516
|
-
if (this.alepha.isReady()) return;
|
|
1517
|
-
reply.headers["Retry-After"] = "5";
|
|
1518
|
-
throw new HttpError({
|
|
1519
|
-
status: 503,
|
|
1520
|
-
message: "Server is not ready yet. Please try again later."
|
|
1521
|
-
});
|
|
1522
|
-
}
|
|
1523
|
-
});
|
|
1524
|
-
};
|
|
1525
|
-
|
|
1526
|
-
//#endregion
|
|
1527
|
-
//#region src/server/constants/routeMethods.ts
|
|
1528
|
-
const routeMethods = [
|
|
1529
|
-
"GET",
|
|
1530
|
-
"POST",
|
|
1531
|
-
"PUT",
|
|
1532
|
-
"PATCH",
|
|
1533
|
-
"DELETE",
|
|
1534
|
-
"HEAD",
|
|
1535
|
-
"OPTIONS",
|
|
1536
|
-
"CONNECT",
|
|
1537
|
-
"TRACE"
|
|
1538
|
-
];
|
|
1539
|
-
|
|
1540
|
-
//#endregion
|
|
1541
|
-
//#region src/server/errors/BadRequestError.ts
|
|
1542
|
-
var BadRequestError = class extends HttpError {
|
|
1543
|
-
constructor(message = "Invalid request body", cause) {
|
|
1544
|
-
super({
|
|
1545
|
-
message,
|
|
1546
|
-
status: 400
|
|
1547
|
-
}, cause);
|
|
1548
|
-
}
|
|
1549
|
-
};
|
|
1550
|
-
|
|
1551
|
-
//#endregion
|
|
1552
|
-
//#region src/server/errors/ConflictError.ts
|
|
1553
|
-
var ConflictError = class extends HttpError {
|
|
1554
|
-
constructor(message = "Entity already exists", cause) {
|
|
1555
|
-
super({
|
|
1556
|
-
message,
|
|
1557
|
-
status: 409
|
|
1558
|
-
}, cause);
|
|
1559
|
-
}
|
|
1560
|
-
};
|
|
1561
|
-
|
|
1562
|
-
//#endregion
|
|
1563
|
-
//#region src/server/errors/ForbiddenError.ts
|
|
1564
|
-
var ForbiddenError = class extends HttpError {
|
|
1565
|
-
constructor(message = "No permission to access this resource", cause) {
|
|
1566
|
-
super({
|
|
1567
|
-
message,
|
|
1568
|
-
status: 403
|
|
1569
|
-
}, cause);
|
|
1570
|
-
}
|
|
1571
|
-
};
|
|
1572
|
-
|
|
1573
|
-
//#endregion
|
|
1574
|
-
//#region src/server/errors/NotFoundError.ts
|
|
1575
|
-
var NotFoundError = class extends HttpError {
|
|
1576
|
-
constructor(message = "Resource not found", cause) {
|
|
1577
|
-
super({
|
|
1578
|
-
message,
|
|
1579
|
-
status: 404
|
|
1580
|
-
}, cause);
|
|
1581
|
-
}
|
|
1582
|
-
};
|
|
1583
|
-
|
|
1584
|
-
//#endregion
|
|
1585
|
-
//#region src/server/errors/UnauthorizedError.ts
|
|
1586
|
-
var UnauthorizedError = class extends HttpError {
|
|
1587
|
-
name = "UnauthorizedError";
|
|
1588
|
-
constructor(message = "Not allowed to access this resource", cause) {
|
|
1589
|
-
super({
|
|
1590
|
-
message,
|
|
1591
|
-
status: 401
|
|
1592
|
-
}, cause);
|
|
1593
|
-
}
|
|
1594
|
-
};
|
|
1595
|
-
|
|
1596
|
-
//#endregion
|
|
1597
|
-
//#region src/server/schemas/okSchema.ts
|
|
1598
|
-
const okSchema = alepha.t.object({
|
|
1599
|
-
ok: alepha.t.boolean({ description: "True when operation succeed" }),
|
|
1600
|
-
id: alepha.t.optional(alepha.t.union([alepha.t.text(), alepha.t.integer()])),
|
|
1601
|
-
count: alepha.t.optional(alepha.t.number({ description: "Number of resources affected" }))
|
|
1602
|
-
}, {
|
|
1603
|
-
title: "Ok",
|
|
1604
|
-
description: "Generic response after a successful operation on a resource"
|
|
1605
|
-
});
|
|
1606
|
-
|
|
1607
|
-
//#endregion
|
|
1608
|
-
//#region src/server/index.ts
|
|
1609
|
-
/**
|
|
1610
|
-
* Provides high-performance HTTP server capabilities with declarative routing and action descriptors.
|
|
1611
|
-
*
|
|
1612
|
-
* The server module enables building REST APIs and web applications using `$route` and `$action` descriptors
|
|
1613
|
-
* on class properties. It provides automatic request/response handling, schema validation, middleware support,
|
|
1614
|
-
* and seamless integration with other Alepha modules for a complete backend solution.
|
|
1615
|
-
*
|
|
1616
|
-
* @see {@link $route}
|
|
1617
|
-
* @see {@link $action}
|
|
1618
|
-
* @module alepha.server
|
|
1619
|
-
*/
|
|
1620
|
-
const AlephaServer = (0, alepha.$module)({
|
|
1621
|
-
name: "alepha.server",
|
|
1622
|
-
descriptors: [$route, $action],
|
|
1623
|
-
services: [
|
|
1624
|
-
ServerProvider,
|
|
1625
|
-
BunHttpServerProvider,
|
|
1626
|
-
NodeHttpServerProvider,
|
|
1627
|
-
ServerBodyParserProvider,
|
|
1628
|
-
ServerLoggerProvider,
|
|
1629
|
-
ServerNotReadyProvider,
|
|
1630
|
-
ServerTimingProvider,
|
|
1631
|
-
HttpClient
|
|
1632
|
-
],
|
|
1633
|
-
register: (alepha$1) => {
|
|
1634
|
-
if (!alepha$1.isServerless() && !alepha$1.isViteDev()) if (alepha$1.isBun()) alepha$1.with({
|
|
1635
|
-
optional: true,
|
|
1636
|
-
provide: ServerProvider,
|
|
1637
|
-
use: BunHttpServerProvider
|
|
1638
|
-
});
|
|
1639
|
-
else alepha$1.with({
|
|
1640
|
-
optional: true,
|
|
1641
|
-
provide: ServerProvider,
|
|
1642
|
-
use: NodeHttpServerProvider
|
|
1643
|
-
});
|
|
1644
|
-
else alepha$1.with(ServerProvider);
|
|
1645
|
-
alepha$1.with(ServerBodyParserProvider);
|
|
1646
|
-
alepha$1.with(ServerLoggerProvider);
|
|
1647
|
-
alepha$1.with(ServerNotReadyProvider);
|
|
1648
|
-
if (!alepha$1.isProduction()) alepha$1.with(ServerTimingProvider);
|
|
1649
|
-
}
|
|
1650
|
-
});
|
|
1651
|
-
|
|
1652
|
-
//#endregion
|
|
1653
|
-
exports.$action = $action;
|
|
1654
|
-
exports.$route = $route;
|
|
1655
|
-
exports.ActionDescriptor = ActionDescriptor;
|
|
1656
|
-
exports.AlephaServer = AlephaServer;
|
|
1657
|
-
exports.BadRequestError = BadRequestError;
|
|
1658
|
-
exports.BunHttpServerProvider = BunHttpServerProvider;
|
|
1659
|
-
exports.ConflictError = ConflictError;
|
|
1660
|
-
exports.ForbiddenError = ForbiddenError;
|
|
1661
|
-
exports.HttpClient = HttpClient;
|
|
1662
|
-
exports.HttpError = HttpError;
|
|
1663
|
-
exports.NodeHttpServerProvider = NodeHttpServerProvider;
|
|
1664
|
-
exports.NotFoundError = NotFoundError;
|
|
1665
|
-
exports.RouteDescriptor = RouteDescriptor;
|
|
1666
|
-
exports.ServerLoggerProvider = ServerLoggerProvider;
|
|
1667
|
-
exports.ServerNotReadyProvider = ServerNotReadyProvider;
|
|
1668
|
-
exports.ServerProvider = ServerProvider;
|
|
1669
|
-
exports.ServerReply = ServerReply;
|
|
1670
|
-
exports.ServerRouterProvider = ServerRouterProvider;
|
|
1671
|
-
exports.ServerTimingProvider = ServerTimingProvider;
|
|
1672
|
-
exports.UnauthorizedError = UnauthorizedError;
|
|
1673
|
-
exports.ValidationError = ValidationError;
|
|
1674
|
-
exports.errorNameByStatus = errorNameByStatus;
|
|
1675
|
-
exports.errorSchema = errorSchema;
|
|
1676
|
-
exports.isHttpError = isHttpError;
|
|
1677
|
-
exports.isMultipart = isMultipart;
|
|
1678
|
-
exports.okSchema = okSchema;
|
|
1679
|
-
exports.routeMethods = routeMethods;
|
|
1680
|
-
//# sourceMappingURL=index.cjs.map
|