create-phoenixjs 0.1.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/index.ts +196 -0
- package/package.json +31 -0
- package/template/README.md +62 -0
- package/template/app/controllers/ExampleController.ts +61 -0
- package/template/artisan +2 -0
- package/template/bootstrap/app.ts +44 -0
- package/template/bunfig.toml +7 -0
- package/template/config/database.ts +25 -0
- package/template/config/plugins.ts +7 -0
- package/template/config/security.ts +158 -0
- package/template/framework/cli/Command.ts +17 -0
- package/template/framework/cli/ConsoleApplication.ts +55 -0
- package/template/framework/cli/artisan.ts +16 -0
- package/template/framework/cli/commands/MakeControllerCommand.ts +41 -0
- package/template/framework/cli/commands/MakeMiddlewareCommand.ts +41 -0
- package/template/framework/cli/commands/MakeModelCommand.ts +36 -0
- package/template/framework/cli/commands/MakeValidatorCommand.ts +42 -0
- package/template/framework/controller/Controller.ts +222 -0
- package/template/framework/core/Application.ts +208 -0
- package/template/framework/core/Container.ts +100 -0
- package/template/framework/core/Kernel.ts +297 -0
- package/template/framework/database/DatabaseAdapter.ts +18 -0
- package/template/framework/database/PrismaAdapter.ts +65 -0
- package/template/framework/database/SqlAdapter.ts +117 -0
- package/template/framework/gateway/Gateway.ts +109 -0
- package/template/framework/gateway/GatewayManager.ts +150 -0
- package/template/framework/gateway/WebSocketAdapter.ts +159 -0
- package/template/framework/gateway/WebSocketGateway.ts +182 -0
- package/template/framework/http/Request.ts +608 -0
- package/template/framework/http/Response.ts +525 -0
- package/template/framework/http/Server.ts +161 -0
- package/template/framework/http/UploadedFile.ts +145 -0
- package/template/framework/middleware/Middleware.ts +50 -0
- package/template/framework/middleware/Pipeline.ts +89 -0
- package/template/framework/plugin/Plugin.ts +26 -0
- package/template/framework/plugin/PluginManager.ts +61 -0
- package/template/framework/routing/RouteRegistry.ts +185 -0
- package/template/framework/routing/Router.ts +280 -0
- package/template/framework/security/CorsMiddleware.ts +151 -0
- package/template/framework/security/CsrfMiddleware.ts +121 -0
- package/template/framework/security/HelmetMiddleware.ts +138 -0
- package/template/framework/security/InputSanitizerMiddleware.ts +134 -0
- package/template/framework/security/RateLimiterMiddleware.ts +189 -0
- package/template/framework/security/SecurityManager.ts +128 -0
- package/template/framework/validation/Validator.ts +482 -0
- package/template/package.json +24 -0
- package/template/routes/api.ts +56 -0
- package/template/server.ts +29 -0
- package/template/tsconfig.json +49 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS - Response Helper
|
|
3
|
+
*
|
|
4
|
+
* Static helper class and fluent builder for creating HTTP responses.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
8
|
+
|
|
9
|
+
export interface ResponseOptions {
|
|
10
|
+
status?: number;
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CookieOptions {
|
|
15
|
+
maxAge?: number;
|
|
16
|
+
expires?: Date;
|
|
17
|
+
path?: string;
|
|
18
|
+
domain?: string;
|
|
19
|
+
secure?: boolean;
|
|
20
|
+
httpOnly?: boolean;
|
|
21
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Cookie {
|
|
25
|
+
name: string;
|
|
26
|
+
value: string;
|
|
27
|
+
options: CookieOptions;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fluent Response Builder
|
|
32
|
+
* Allows chaining methods to build a response
|
|
33
|
+
*/
|
|
34
|
+
export class ResponseBuilder {
|
|
35
|
+
private statusCode = 200;
|
|
36
|
+
private responseHeaders: Map<string, string> = new Map();
|
|
37
|
+
private responseCookies: Cookie[] = [];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set the response status code
|
|
41
|
+
*/
|
|
42
|
+
status(code: number): this {
|
|
43
|
+
this.statusCode = code;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Add a header to the response
|
|
49
|
+
*/
|
|
50
|
+
header(name: string, value: string): this {
|
|
51
|
+
this.responseHeaders.set(name, value);
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Add multiple headers
|
|
57
|
+
*/
|
|
58
|
+
withHeaders(headers: Record<string, string>): this {
|
|
59
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
60
|
+
this.responseHeaders.set(name, value);
|
|
61
|
+
}
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Set a cookie
|
|
67
|
+
*/
|
|
68
|
+
cookie(name: string, value: string, options: CookieOptions = {}): this {
|
|
69
|
+
this.responseCookies.push({ name, value, options });
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Remove a cookie (set it to expire immediately)
|
|
75
|
+
*/
|
|
76
|
+
withoutCookie(name: string, options: CookieOptions = {}): this {
|
|
77
|
+
this.responseCookies.push({
|
|
78
|
+
name,
|
|
79
|
+
value: '',
|
|
80
|
+
options: { ...options, maxAge: 0 },
|
|
81
|
+
});
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Set CORS headers
|
|
87
|
+
*/
|
|
88
|
+
cors(origin: string = '*', options: {
|
|
89
|
+
methods?: string[];
|
|
90
|
+
headers?: string[];
|
|
91
|
+
credentials?: boolean;
|
|
92
|
+
maxAge?: number;
|
|
93
|
+
} = {}): this {
|
|
94
|
+
this.header('Access-Control-Allow-Origin', origin);
|
|
95
|
+
|
|
96
|
+
if (options.methods) {
|
|
97
|
+
this.header('Access-Control-Allow-Methods', options.methods.join(', '));
|
|
98
|
+
}
|
|
99
|
+
if (options.headers) {
|
|
100
|
+
this.header('Access-Control-Allow-Headers', options.headers.join(', '));
|
|
101
|
+
}
|
|
102
|
+
if (options.credentials) {
|
|
103
|
+
this.header('Access-Control-Allow-Credentials', 'true');
|
|
104
|
+
}
|
|
105
|
+
if (options.maxAge) {
|
|
106
|
+
this.header('Access-Control-Max-Age', options.maxAge.toString());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Set cache control headers
|
|
114
|
+
*/
|
|
115
|
+
cache(maxAge: number, options: {
|
|
116
|
+
public?: boolean;
|
|
117
|
+
private?: boolean;
|
|
118
|
+
noStore?: boolean;
|
|
119
|
+
mustRevalidate?: boolean;
|
|
120
|
+
} = {}): this {
|
|
121
|
+
const directives: string[] = [];
|
|
122
|
+
|
|
123
|
+
if (options.public) directives.push('public');
|
|
124
|
+
if (options.private) directives.push('private');
|
|
125
|
+
if (options.noStore) directives.push('no-store');
|
|
126
|
+
if (options.mustRevalidate) directives.push('must-revalidate');
|
|
127
|
+
|
|
128
|
+
directives.push(`max-age=${maxAge}`);
|
|
129
|
+
|
|
130
|
+
this.header('Cache-Control', directives.join(', '));
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Disable caching
|
|
136
|
+
*/
|
|
137
|
+
noCache(): this {
|
|
138
|
+
this.header('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
139
|
+
this.header('Pragma', 'no-cache');
|
|
140
|
+
this.header('Expires', '0');
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build and return a JSON response
|
|
146
|
+
*/
|
|
147
|
+
json(data: unknown): Response {
|
|
148
|
+
this.header('Content-Type', 'application/json');
|
|
149
|
+
return this.build(JSON.stringify(data));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Build and return a text response
|
|
154
|
+
*/
|
|
155
|
+
text(content: string): Response {
|
|
156
|
+
this.header('Content-Type', 'text/plain');
|
|
157
|
+
return this.build(content);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build and return an HTML response
|
|
162
|
+
*/
|
|
163
|
+
html(content: string): Response {
|
|
164
|
+
this.header('Content-Type', 'text/html');
|
|
165
|
+
return this.build(content);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build and return an XML response
|
|
170
|
+
*/
|
|
171
|
+
xml(content: string): Response {
|
|
172
|
+
this.header('Content-Type', 'application/xml');
|
|
173
|
+
return this.build(content);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Build and return a file download response
|
|
178
|
+
*/
|
|
179
|
+
download(content: Buffer | string | Uint8Array, filename: string, mimeType?: string): Response {
|
|
180
|
+
const contentType = mimeType ?? 'application/octet-stream';
|
|
181
|
+
this.header('Content-Type', contentType);
|
|
182
|
+
this.header('Content-Disposition', `attachment; filename="${filename}"`);
|
|
183
|
+
return this.build(content);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Build and return an inline file response (displayed in browser)
|
|
188
|
+
*/
|
|
189
|
+
file(content: Buffer | Uint8Array, mimeType: string, filename?: string): Response {
|
|
190
|
+
this.header('Content-Type', mimeType);
|
|
191
|
+
if (filename) {
|
|
192
|
+
this.header('Content-Disposition', `inline; filename="${filename}"`);
|
|
193
|
+
}
|
|
194
|
+
return this.build(content);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Build and return a streaming response
|
|
199
|
+
*/
|
|
200
|
+
stream(readable: ReadableStream, mimeType: string = 'application/octet-stream'): Response {
|
|
201
|
+
this.header('Content-Type', mimeType);
|
|
202
|
+
return new Response(readable, {
|
|
203
|
+
status: this.statusCode,
|
|
204
|
+
headers: this.buildHeaders(),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Build and return a redirect response
|
|
210
|
+
*/
|
|
211
|
+
redirect(url: string, permanent: boolean = false): Response {
|
|
212
|
+
this.statusCode = permanent ? 301 : 302;
|
|
213
|
+
this.header('Location', url);
|
|
214
|
+
return this.build(null);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Build and return a 204 No Content response
|
|
219
|
+
*/
|
|
220
|
+
noContent(): Response {
|
|
221
|
+
this.statusCode = 204;
|
|
222
|
+
return this.build(null);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Build the final response
|
|
227
|
+
*/
|
|
228
|
+
build(body?: string | ArrayBuffer | Uint8Array | ReadableStream | null): Response {
|
|
229
|
+
return new Response(body, {
|
|
230
|
+
status: this.statusCode,
|
|
231
|
+
headers: this.buildHeaders(),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Build headers including cookies
|
|
237
|
+
*/
|
|
238
|
+
private buildHeaders(): Headers {
|
|
239
|
+
const headers = new Headers();
|
|
240
|
+
|
|
241
|
+
for (const [name, value] of this.responseHeaders) {
|
|
242
|
+
headers.set(name, value);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Add cookies as Set-Cookie headers
|
|
246
|
+
for (const cookie of this.responseCookies) {
|
|
247
|
+
headers.append('Set-Cookie', this.serializeCookie(cookie));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return headers;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Serialize a cookie to a Set-Cookie header value
|
|
255
|
+
*/
|
|
256
|
+
private serializeCookie(cookie: Cookie): string {
|
|
257
|
+
const parts = [`${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`];
|
|
258
|
+
const opts = cookie.options;
|
|
259
|
+
|
|
260
|
+
if (opts.maxAge !== undefined) {
|
|
261
|
+
parts.push(`Max-Age=${opts.maxAge}`);
|
|
262
|
+
}
|
|
263
|
+
if (opts.expires) {
|
|
264
|
+
parts.push(`Expires=${opts.expires.toUTCString()}`);
|
|
265
|
+
}
|
|
266
|
+
if (opts.path) {
|
|
267
|
+
parts.push(`Path=${opts.path}`);
|
|
268
|
+
}
|
|
269
|
+
if (opts.domain) {
|
|
270
|
+
parts.push(`Domain=${opts.domain}`);
|
|
271
|
+
}
|
|
272
|
+
if (opts.secure) {
|
|
273
|
+
parts.push('Secure');
|
|
274
|
+
}
|
|
275
|
+
if (opts.httpOnly) {
|
|
276
|
+
parts.push('HttpOnly');
|
|
277
|
+
}
|
|
278
|
+
if (opts.sameSite) {
|
|
279
|
+
parts.push(`SameSite=${opts.sameSite}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return parts.join('; ');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Static Response Helper
|
|
288
|
+
* Provides shorthand methods for common responses
|
|
289
|
+
*/
|
|
290
|
+
export class FrameworkResponse {
|
|
291
|
+
/**
|
|
292
|
+
* Create a new ResponseBuilder for fluent API
|
|
293
|
+
*/
|
|
294
|
+
static create(): ResponseBuilder {
|
|
295
|
+
return new ResponseBuilder();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Create a JSON response
|
|
300
|
+
*/
|
|
301
|
+
static json(data: unknown, status = 200, headers: Record<string, string> = {}): Response {
|
|
302
|
+
return new Response(JSON.stringify(data), {
|
|
303
|
+
status,
|
|
304
|
+
headers: {
|
|
305
|
+
'Content-Type': 'application/json',
|
|
306
|
+
...headers,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Create a text response
|
|
313
|
+
*/
|
|
314
|
+
static text(content: string, status = 200, headers: Record<string, string> = {}): Response {
|
|
315
|
+
return new Response(content, {
|
|
316
|
+
status,
|
|
317
|
+
headers: {
|
|
318
|
+
'Content-Type': 'text/plain',
|
|
319
|
+
...headers,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Create an HTML response
|
|
326
|
+
*/
|
|
327
|
+
static html(content: string, status = 200, headers: Record<string, string> = {}): Response {
|
|
328
|
+
return new Response(content, {
|
|
329
|
+
status,
|
|
330
|
+
headers: {
|
|
331
|
+
'Content-Type': 'text/html',
|
|
332
|
+
...headers,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Create an XML response
|
|
339
|
+
*/
|
|
340
|
+
static xml(content: string, status = 200, headers: Record<string, string> = {}): Response {
|
|
341
|
+
return new Response(content, {
|
|
342
|
+
status,
|
|
343
|
+
headers: {
|
|
344
|
+
'Content-Type': 'application/xml',
|
|
345
|
+
...headers,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Create a redirect response
|
|
352
|
+
*/
|
|
353
|
+
static redirect(url: string, status = 302): Response {
|
|
354
|
+
return new Response(null, {
|
|
355
|
+
status,
|
|
356
|
+
headers: {
|
|
357
|
+
Location: url,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Create a permanent redirect (301)
|
|
364
|
+
*/
|
|
365
|
+
static permanentRedirect(url: string): Response {
|
|
366
|
+
return FrameworkResponse.redirect(url, 301);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Create a 204 No Content response
|
|
371
|
+
*/
|
|
372
|
+
static noContent(): Response {
|
|
373
|
+
return new Response(null, { status: 204 });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Create an error response
|
|
378
|
+
*/
|
|
379
|
+
static error(message: string, status = 500, details?: Record<string, unknown>): Response {
|
|
380
|
+
const body: Record<string, unknown> = {
|
|
381
|
+
error: true,
|
|
382
|
+
message,
|
|
383
|
+
};
|
|
384
|
+
if (details) {
|
|
385
|
+
body.details = details;
|
|
386
|
+
}
|
|
387
|
+
return FrameworkResponse.json(body, status);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Create a 400 Bad Request response
|
|
392
|
+
*/
|
|
393
|
+
static badRequest(message: string, details?: Record<string, unknown>): Response {
|
|
394
|
+
return FrameworkResponse.error(message, 400, details);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Create a 401 Unauthorized response
|
|
399
|
+
*/
|
|
400
|
+
static unauthorized(message = 'Unauthorized'): Response {
|
|
401
|
+
return FrameworkResponse.error(message, 401);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Create a 403 Forbidden response
|
|
406
|
+
*/
|
|
407
|
+
static forbidden(message = 'Forbidden'): Response {
|
|
408
|
+
return FrameworkResponse.error(message, 403);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Create a 404 Not Found response
|
|
413
|
+
*/
|
|
414
|
+
static notFound(message = 'Not Found'): Response {
|
|
415
|
+
return FrameworkResponse.error(message, 404);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Create a 405 Method Not Allowed response
|
|
420
|
+
*/
|
|
421
|
+
static methodNotAllowed(message = 'Method Not Allowed', allowed?: string[]): Response {
|
|
422
|
+
const headers: Record<string, string> = {};
|
|
423
|
+
if (allowed) {
|
|
424
|
+
headers['Allow'] = allowed.join(', ');
|
|
425
|
+
}
|
|
426
|
+
return new Response(JSON.stringify({ error: true, message }), {
|
|
427
|
+
status: 405,
|
|
428
|
+
headers: {
|
|
429
|
+
'Content-Type': 'application/json',
|
|
430
|
+
...headers,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Create a 409 Conflict response
|
|
437
|
+
*/
|
|
438
|
+
static conflict(message = 'Conflict', details?: Record<string, unknown>): Response {
|
|
439
|
+
return FrameworkResponse.error(message, 409, details);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Create a 422 Unprocessable Entity response (validation errors)
|
|
444
|
+
*/
|
|
445
|
+
static validationError(errors: Record<string, string[]>): Response {
|
|
446
|
+
return FrameworkResponse.json({
|
|
447
|
+
error: true,
|
|
448
|
+
message: 'Validation failed',
|
|
449
|
+
errors,
|
|
450
|
+
}, 422);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Create a 429 Too Many Requests response
|
|
455
|
+
*/
|
|
456
|
+
static tooManyRequests(message = 'Too Many Requests', retryAfter?: number): Response {
|
|
457
|
+
const headers: Record<string, string> = {};
|
|
458
|
+
if (retryAfter) {
|
|
459
|
+
headers['Retry-After'] = retryAfter.toString();
|
|
460
|
+
}
|
|
461
|
+
return new Response(JSON.stringify({ error: true, message }), {
|
|
462
|
+
status: 429,
|
|
463
|
+
headers: {
|
|
464
|
+
'Content-Type': 'application/json',
|
|
465
|
+
...headers,
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Create a 500 Internal Server Error response
|
|
472
|
+
*/
|
|
473
|
+
static serverError(message = 'Internal Server Error', details?: Record<string, unknown>): Response {
|
|
474
|
+
return FrameworkResponse.error(message, 500, details);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Create a 502 Bad Gateway response
|
|
479
|
+
*/
|
|
480
|
+
static badGateway(message = 'Bad Gateway'): Response {
|
|
481
|
+
return FrameworkResponse.error(message, 502);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Create a 503 Service Unavailable response
|
|
486
|
+
*/
|
|
487
|
+
static serviceUnavailable(message = 'Service Unavailable', retryAfter?: number): Response {
|
|
488
|
+
const headers: Record<string, string> = {};
|
|
489
|
+
if (retryAfter) {
|
|
490
|
+
headers['Retry-After'] = retryAfter.toString();
|
|
491
|
+
}
|
|
492
|
+
return new Response(JSON.stringify({ error: true, message }), {
|
|
493
|
+
status: 503,
|
|
494
|
+
headers: {
|
|
495
|
+
'Content-Type': 'application/json',
|
|
496
|
+
...headers,
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Create a file download response
|
|
503
|
+
*/
|
|
504
|
+
static download(content: Buffer | string | Uint8Array, filename: string, mimeType = 'application/octet-stream'): Response {
|
|
505
|
+
return new Response(content, {
|
|
506
|
+
status: 200,
|
|
507
|
+
headers: {
|
|
508
|
+
'Content-Type': mimeType,
|
|
509
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Create a streaming response
|
|
516
|
+
*/
|
|
517
|
+
static stream(readable: ReadableStream, mimeType = 'application/octet-stream'): Response {
|
|
518
|
+
return new Response(readable, {
|
|
519
|
+
status: 200,
|
|
520
|
+
headers: {
|
|
521
|
+
'Content-Type': mimeType,
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS - HTTP Server
|
|
3
|
+
*
|
|
4
|
+
* Wraps Bun.serve with lifecycle hooks and configuration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Kernel } from '@framework/core/Kernel';
|
|
8
|
+
import { Application } from '@framework/core/Application';
|
|
9
|
+
import { WebSocketAdapter } from '@framework/gateway/WebSocketAdapter';
|
|
10
|
+
import type { WebSocketData } from '@framework/gateway/Gateway';
|
|
11
|
+
|
|
12
|
+
// Use the inferred type from Bun.serve to avoid generic type issues
|
|
13
|
+
type BunServer = ReturnType<typeof Bun.serve>;
|
|
14
|
+
|
|
15
|
+
export interface ServerConfig {
|
|
16
|
+
port: number;
|
|
17
|
+
host: string;
|
|
18
|
+
development?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ServerHooks {
|
|
22
|
+
onStart?: (server: BunServer) => void;
|
|
23
|
+
onStop?: () => void;
|
|
24
|
+
onError?: (error: Error) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class Server {
|
|
28
|
+
private kernel: Kernel;
|
|
29
|
+
private app: Application;
|
|
30
|
+
private config: ServerConfig;
|
|
31
|
+
private hooks: ServerHooks;
|
|
32
|
+
private server: BunServer | null = null;
|
|
33
|
+
private wsAdapter: WebSocketAdapter;
|
|
34
|
+
|
|
35
|
+
constructor(kernel: Kernel, config?: Partial<ServerConfig>, hooks?: ServerHooks) {
|
|
36
|
+
this.kernel = kernel;
|
|
37
|
+
this.app = kernel.getApplication();
|
|
38
|
+
this.config = {
|
|
39
|
+
port: config?.port ?? this.app.getConfig('port'),
|
|
40
|
+
host: config?.host ?? this.app.getConfig('host'),
|
|
41
|
+
development: config?.development ?? this.app.getConfig('env') === 'development',
|
|
42
|
+
};
|
|
43
|
+
this.hooks = hooks ?? {};
|
|
44
|
+
this.wsAdapter = new WebSocketAdapter();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Start the server
|
|
49
|
+
*/
|
|
50
|
+
start(): BunServer {
|
|
51
|
+
const kernel = this.kernel;
|
|
52
|
+
const hooks = this.hooks;
|
|
53
|
+
const wsAdapter = this.wsAdapter;
|
|
54
|
+
|
|
55
|
+
// Initialize WebSocket gateways
|
|
56
|
+
const gatewayManager = this.app.getGatewayManager();
|
|
57
|
+
gatewayManager.registerAdapter(wsAdapter);
|
|
58
|
+
wsAdapter.start(gatewayManager.getByTransport('websocket'));
|
|
59
|
+
|
|
60
|
+
this.server = Bun.serve<WebSocketData>({
|
|
61
|
+
port: this.config.port,
|
|
62
|
+
hostname: this.config.host,
|
|
63
|
+
development: this.config.development,
|
|
64
|
+
|
|
65
|
+
fetch: async (request: Request, server: BunServer): Promise<Response | undefined> => {
|
|
66
|
+
// Check for WebSocket upgrade
|
|
67
|
+
const url = new URL(request.url);
|
|
68
|
+
if (wsAdapter.hasGatewayForPath(url.pathname)) {
|
|
69
|
+
const upgraded = wsAdapter.handleUpgrade(request, server);
|
|
70
|
+
if (upgraded) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return new Response('WebSocket upgrade failed', { status: 400 });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return kernel.handle(request);
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
websocket: wsAdapter.getWebSocketConfig(),
|
|
80
|
+
|
|
81
|
+
error: (error: Error): Response => {
|
|
82
|
+
if (hooks.onError) {
|
|
83
|
+
hooks.onError(error);
|
|
84
|
+
}
|
|
85
|
+
console.error('[Server Error]', error);
|
|
86
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
wsAdapter.setServer(this.server);
|
|
91
|
+
|
|
92
|
+
if (this.hooks.onStart) {
|
|
93
|
+
this.hooks.onStart(this.server);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.logStartup();
|
|
97
|
+
return this.server;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Log server startup information
|
|
102
|
+
*/
|
|
103
|
+
private logStartup(): void {
|
|
104
|
+
const appName = this.app.getConfig('name');
|
|
105
|
+
const env = this.app.environment();
|
|
106
|
+
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(`🚀 ${appName} started successfully!`);
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log(` Local: http://${this.config.host}:${this.config.port}`);
|
|
111
|
+
console.log(` Environment: ${env}`);
|
|
112
|
+
console.log(` Debug: ${this.app.isDebug() ? 'enabled' : 'disabled'}`);
|
|
113
|
+
console.log('');
|
|
114
|
+
console.log(' Press Ctrl+C to stop');
|
|
115
|
+
console.log('');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Stop the server
|
|
120
|
+
*/
|
|
121
|
+
stop(): void {
|
|
122
|
+
if (this.server) {
|
|
123
|
+
this.server.stop();
|
|
124
|
+
this.server = null;
|
|
125
|
+
|
|
126
|
+
if (this.hooks.onStop) {
|
|
127
|
+
this.hooks.onStop();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log('Server stopped');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the underlying Bun server
|
|
136
|
+
*/
|
|
137
|
+
getServer(): BunServer | null {
|
|
138
|
+
return this.server;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if server is running
|
|
143
|
+
*/
|
|
144
|
+
isRunning(): boolean {
|
|
145
|
+
return this.server !== null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get the port
|
|
150
|
+
*/
|
|
151
|
+
getPort(): number {
|
|
152
|
+
return this.config.port;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get the host
|
|
157
|
+
*/
|
|
158
|
+
getHost(): string {
|
|
159
|
+
return this.config.host;
|
|
160
|
+
}
|
|
161
|
+
}
|