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.
Files changed (49) hide show
  1. package/index.ts +196 -0
  2. package/package.json +31 -0
  3. package/template/README.md +62 -0
  4. package/template/app/controllers/ExampleController.ts +61 -0
  5. package/template/artisan +2 -0
  6. package/template/bootstrap/app.ts +44 -0
  7. package/template/bunfig.toml +7 -0
  8. package/template/config/database.ts +25 -0
  9. package/template/config/plugins.ts +7 -0
  10. package/template/config/security.ts +158 -0
  11. package/template/framework/cli/Command.ts +17 -0
  12. package/template/framework/cli/ConsoleApplication.ts +55 -0
  13. package/template/framework/cli/artisan.ts +16 -0
  14. package/template/framework/cli/commands/MakeControllerCommand.ts +41 -0
  15. package/template/framework/cli/commands/MakeMiddlewareCommand.ts +41 -0
  16. package/template/framework/cli/commands/MakeModelCommand.ts +36 -0
  17. package/template/framework/cli/commands/MakeValidatorCommand.ts +42 -0
  18. package/template/framework/controller/Controller.ts +222 -0
  19. package/template/framework/core/Application.ts +208 -0
  20. package/template/framework/core/Container.ts +100 -0
  21. package/template/framework/core/Kernel.ts +297 -0
  22. package/template/framework/database/DatabaseAdapter.ts +18 -0
  23. package/template/framework/database/PrismaAdapter.ts +65 -0
  24. package/template/framework/database/SqlAdapter.ts +117 -0
  25. package/template/framework/gateway/Gateway.ts +109 -0
  26. package/template/framework/gateway/GatewayManager.ts +150 -0
  27. package/template/framework/gateway/WebSocketAdapter.ts +159 -0
  28. package/template/framework/gateway/WebSocketGateway.ts +182 -0
  29. package/template/framework/http/Request.ts +608 -0
  30. package/template/framework/http/Response.ts +525 -0
  31. package/template/framework/http/Server.ts +161 -0
  32. package/template/framework/http/UploadedFile.ts +145 -0
  33. package/template/framework/middleware/Middleware.ts +50 -0
  34. package/template/framework/middleware/Pipeline.ts +89 -0
  35. package/template/framework/plugin/Plugin.ts +26 -0
  36. package/template/framework/plugin/PluginManager.ts +61 -0
  37. package/template/framework/routing/RouteRegistry.ts +185 -0
  38. package/template/framework/routing/Router.ts +280 -0
  39. package/template/framework/security/CorsMiddleware.ts +151 -0
  40. package/template/framework/security/CsrfMiddleware.ts +121 -0
  41. package/template/framework/security/HelmetMiddleware.ts +138 -0
  42. package/template/framework/security/InputSanitizerMiddleware.ts +134 -0
  43. package/template/framework/security/RateLimiterMiddleware.ts +189 -0
  44. package/template/framework/security/SecurityManager.ts +128 -0
  45. package/template/framework/validation/Validator.ts +482 -0
  46. package/template/package.json +24 -0
  47. package/template/routes/api.ts +56 -0
  48. package/template/server.ts +29 -0
  49. package/template/tsconfig.json +49 -0
@@ -0,0 +1,608 @@
1
+ /**
2
+ * PhoenixJS - Request Wrapper
3
+ *
4
+ * Wraps the native Bun Request with helpful methods.
5
+ * Inspired by Laravel's Request class.
6
+ */
7
+
8
+ import { UploadedFile } from '@framework/http/UploadedFile';
9
+
10
+ export class FrameworkRequest {
11
+ private request: Request;
12
+ private parsedUrl: URL;
13
+ private queryParams: URLSearchParams;
14
+ private pathParams: Record<string, string> = {};
15
+ private parsedBody: unknown = null;
16
+ private bodyParsed = false;
17
+ private bodyText: string | null = null;
18
+ private bodyTextParsed = false;
19
+ private parsedCookies: Record<string, string> | null = null;
20
+ private parsedFiles: Map<string, UploadedFile[]> | null = null;
21
+
22
+ constructor(request: Request) {
23
+ this.request = request;
24
+ this.parsedUrl = new URL(request.url);
25
+ this.queryParams = this.parsedUrl.searchParams;
26
+ }
27
+
28
+ // ==========================================
29
+ // Basic Request Information
30
+ // ==========================================
31
+
32
+ /**
33
+ * Get the original Request object
34
+ */
35
+ getOriginal(): Request {
36
+ return this.request;
37
+ }
38
+
39
+ /**
40
+ * Get the request method
41
+ */
42
+ method(): string {
43
+ return this.request.method;
44
+ }
45
+
46
+ /**
47
+ * Get the full URL
48
+ */
49
+ url(): string {
50
+ return this.request.url;
51
+ }
52
+
53
+ /**
54
+ * Get the pathname
55
+ */
56
+ path(): string {
57
+ return this.parsedUrl.pathname;
58
+ }
59
+
60
+ /**
61
+ * Get the hostname
62
+ */
63
+ host(): string {
64
+ return this.parsedUrl.host;
65
+ }
66
+
67
+ /**
68
+ * Get the protocol (http or https)
69
+ */
70
+ protocol(): string {
71
+ return this.parsedUrl.protocol.replace(':', '');
72
+ }
73
+
74
+ /**
75
+ * Check if the request is secure (HTTPS)
76
+ */
77
+ secure(): boolean {
78
+ return this.parsedUrl.protocol === 'https:';
79
+ }
80
+
81
+ // ==========================================
82
+ // Query Parameters
83
+ // ==========================================
84
+
85
+ /**
86
+ * Get all query parameters
87
+ */
88
+ query(): Record<string, string> {
89
+ const result: Record<string, string> = {};
90
+ this.queryParams.forEach((value, key) => {
91
+ result[key] = value;
92
+ });
93
+ return result;
94
+ }
95
+
96
+ /**
97
+ * Get a specific query parameter
98
+ */
99
+ queryParam(key: string, defaultValue?: string): string | undefined {
100
+ return this.queryParams.get(key) ?? defaultValue;
101
+ }
102
+
103
+ // ==========================================
104
+ // Path Parameters
105
+ // ==========================================
106
+
107
+ /**
108
+ * Set path parameters (used by router)
109
+ */
110
+ setParams(params: Record<string, string>): void {
111
+ this.pathParams = params;
112
+ }
113
+
114
+ /**
115
+ * Get all path parameters
116
+ */
117
+ params(): Record<string, string> {
118
+ return { ...this.pathParams };
119
+ }
120
+
121
+ /**
122
+ * Get a specific path parameter
123
+ */
124
+ param(key: string, defaultValue?: string): string | undefined {
125
+ return this.pathParams[key] ?? defaultValue;
126
+ }
127
+
128
+ // ==========================================
129
+ // Headers
130
+ // ==========================================
131
+
132
+ /**
133
+ * Get request headers
134
+ */
135
+ headers(): Headers {
136
+ return this.request.headers;
137
+ }
138
+
139
+ /**
140
+ * Get a specific header
141
+ */
142
+ header(name: string, defaultValue?: string): string | undefined {
143
+ return this.request.headers.get(name) ?? defaultValue;
144
+ }
145
+
146
+ /**
147
+ * Check if a header exists
148
+ */
149
+ hasHeader(name: string): boolean {
150
+ return this.request.headers.has(name);
151
+ }
152
+
153
+ // ==========================================
154
+ // Body Parsing
155
+ // ==========================================
156
+
157
+ /**
158
+ * Parse and return the request body as JSON
159
+ */
160
+ async json<T = unknown>(): Promise<T> {
161
+ if (!this.bodyParsed) {
162
+ try {
163
+ const text = await this.text();
164
+ this.parsedBody = JSON.parse(text);
165
+ } catch {
166
+ this.parsedBody = null;
167
+ }
168
+ this.bodyParsed = true;
169
+ }
170
+ return this.parsedBody as T;
171
+ }
172
+
173
+ /**
174
+ * Get the request body as text
175
+ */
176
+ async text(): Promise<string> {
177
+ if (!this.bodyTextParsed) {
178
+ this.bodyText = await this.request.text();
179
+ this.bodyTextParsed = true;
180
+ }
181
+ return this.bodyText ?? '';
182
+ }
183
+
184
+ /**
185
+ * Get the request body as FormData
186
+ */
187
+ async formData() {
188
+ return this.request.formData();
189
+ }
190
+
191
+ // ==========================================
192
+ // Input Methods (Laravel-like)
193
+ // ==========================================
194
+
195
+ /**
196
+ * Get all input (query + body merged)
197
+ * Body parameters override query parameters
198
+ */
199
+ async all(): Promise<Record<string, unknown>> {
200
+ const queryData = this.query();
201
+ const bodyData = await this.json<Record<string, unknown>>() ?? {};
202
+ return { ...queryData, ...bodyData };
203
+ }
204
+
205
+ /**
206
+ * Get an input value from query or body
207
+ * If no key is provided, returns all input
208
+ */
209
+ async input<T = unknown>(key?: string, defaultValue?: T): Promise<T | Record<string, unknown>> {
210
+ const allInput = await this.all();
211
+
212
+ if (key === undefined) {
213
+ return allInput;
214
+ }
215
+
216
+ // Support dot notation for nested values
217
+ const value = this.getNestedValue(allInput, key);
218
+ return (value !== undefined ? value : defaultValue) as T;
219
+ }
220
+
221
+ /**
222
+ * Check if input has given key(s)
223
+ */
224
+ async has(...keys: string[]): Promise<boolean> {
225
+ const allInput = await this.all();
226
+ return keys.every(key => this.getNestedValue(allInput, key) !== undefined);
227
+ }
228
+
229
+ /**
230
+ * Check if input has non-empty value for given key(s)
231
+ */
232
+ async filled(...keys: string[]): Promise<boolean> {
233
+ const allInput = await this.all();
234
+ return keys.every(key => {
235
+ const value = this.getNestedValue(allInput, key);
236
+ return value !== undefined && value !== null && value !== '';
237
+ });
238
+ }
239
+
240
+ /**
241
+ * Check if any of the given keys are missing
242
+ */
243
+ async missing(...keys: string[]): Promise<boolean> {
244
+ return !(await this.has(...keys));
245
+ }
246
+
247
+ /**
248
+ * Get only specified keys from input
249
+ */
250
+ async only(...keys: string[]): Promise<Record<string, unknown>> {
251
+ const allInput = await this.all();
252
+ const result: Record<string, unknown> = {};
253
+
254
+ for (const key of keys) {
255
+ const value = this.getNestedValue(allInput, key);
256
+ if (value !== undefined) {
257
+ result[key] = value;
258
+ }
259
+ }
260
+
261
+ return result;
262
+ }
263
+
264
+ /**
265
+ * Get all input except specified keys
266
+ */
267
+ async except(...keys: string[]): Promise<Record<string, unknown>> {
268
+ const allInput = await this.all();
269
+ const result: Record<string, unknown> = { ...allInput };
270
+
271
+ for (const key of keys) {
272
+ delete result[key];
273
+ }
274
+
275
+ return result;
276
+ }
277
+
278
+ /**
279
+ * Get nested value using dot notation
280
+ */
281
+ private getNestedValue(obj: Record<string, unknown>, path: string): unknown {
282
+ const keys = path.split('.');
283
+ let current: unknown = obj;
284
+
285
+ for (const key of keys) {
286
+ if (current === null || current === undefined || typeof current !== 'object') {
287
+ return undefined;
288
+ }
289
+ current = (current as Record<string, unknown>)[key];
290
+ }
291
+
292
+ return current;
293
+ }
294
+
295
+ // ==========================================
296
+ // Authentication Helpers
297
+ // ==========================================
298
+
299
+ /**
300
+ * Get the Bearer token from Authorization header
301
+ */
302
+ bearerToken(): string | null {
303
+ const auth = this.header('authorization');
304
+ if (!auth || !auth.toLowerCase().startsWith('bearer ')) {
305
+ return null;
306
+ }
307
+ return auth.substring(7).trim();
308
+ }
309
+
310
+ /**
311
+ * Get Basic auth credentials
312
+ */
313
+ basicCredentials(): { username: string; password: string } | null {
314
+ const auth = this.header('authorization');
315
+ if (!auth || !auth.toLowerCase().startsWith('basic ')) {
316
+ return null;
317
+ }
318
+
319
+ try {
320
+ const decoded = Buffer.from(auth.substring(6), 'base64').toString('utf-8');
321
+ const [username, password] = decoded.split(':');
322
+ return { username, password };
323
+ } catch {
324
+ return null;
325
+ }
326
+ }
327
+
328
+ // ==========================================
329
+ // Client Information
330
+ // ==========================================
331
+
332
+ /**
333
+ * Get the client IP address
334
+ */
335
+ ip(): string | null {
336
+ // Check common proxy headers first
337
+ const xForwardedFor = this.header('x-forwarded-for');
338
+ if (xForwardedFor) {
339
+ return xForwardedFor.split(',')[0].trim();
340
+ }
341
+
342
+ const xRealIp = this.header('x-real-ip');
343
+ if (xRealIp) {
344
+ return xRealIp;
345
+ }
346
+
347
+ // CF-Connecting-IP for Cloudflare
348
+ const cfConnectingIp = this.header('cf-connecting-ip');
349
+ if (cfConnectingIp) {
350
+ return cfConnectingIp;
351
+ }
352
+
353
+ return null;
354
+ }
355
+
356
+ /**
357
+ * Get all possible IP addresses
358
+ */
359
+ ips(): string[] {
360
+ const xForwardedFor = this.header('x-forwarded-for');
361
+ if (xForwardedFor) {
362
+ return xForwardedFor.split(',').map(ip => ip.trim());
363
+ }
364
+
365
+ const ip = this.ip();
366
+ return ip ? [ip] : [];
367
+ }
368
+
369
+ /**
370
+ * Get the User-Agent header
371
+ */
372
+ userAgent(): string | null {
373
+ return this.header('user-agent') ?? null;
374
+ }
375
+
376
+ /**
377
+ * Get the referer URL
378
+ */
379
+ referer(): string | null {
380
+ return this.header('referer') ?? this.header('referrer') ?? null;
381
+ }
382
+
383
+ // ==========================================
384
+ // Route Matching
385
+ // ==========================================
386
+
387
+ /**
388
+ * Check if the path matches a pattern
389
+ * Supports wildcards: /users/* matches /users/123
390
+ */
391
+ is(pattern: string): boolean {
392
+ const path = this.path();
393
+ const regexPattern = pattern
394
+ .replace(/\*/g, '.*')
395
+ .replace(/\//g, '\\/');
396
+ const regex = new RegExp(`^${regexPattern}$`);
397
+ return regex.test(path);
398
+ }
399
+
400
+ /**
401
+ * Check if the HTTP method matches
402
+ */
403
+ isMethod(method: string | string[]): boolean {
404
+ const currentMethod = this.method().toUpperCase();
405
+ if (Array.isArray(method)) {
406
+ return method.some(m => m.toUpperCase() === currentMethod);
407
+ }
408
+ return method.toUpperCase() === currentMethod;
409
+ }
410
+
411
+ // ==========================================
412
+ // Content Type Detection
413
+ // ==========================================
414
+
415
+ /**
416
+ * Check if the request expects JSON
417
+ */
418
+ expectsJson(): boolean {
419
+ const accept = this.header('accept', '');
420
+ return accept !== undefined && accept.includes('application/json');
421
+ }
422
+
423
+ /**
424
+ * Check if the request content is JSON
425
+ */
426
+ isJson(): boolean {
427
+ const contentType = this.header('content-type', '');
428
+ return contentType !== undefined && contentType.includes('application/json');
429
+ }
430
+
431
+ /**
432
+ * Check if request is form data
433
+ */
434
+ isFormData(): boolean {
435
+ const contentType = this.contentType() ?? '';
436
+ return contentType.includes('multipart/form-data') ||
437
+ contentType.includes('application/x-www-form-urlencoded');
438
+ }
439
+
440
+ /**
441
+ * Get the content type
442
+ */
443
+ contentType(): string | undefined {
444
+ return this.header('content-type');
445
+ }
446
+
447
+ /**
448
+ * Check if request wants HTML
449
+ */
450
+ wantsHtml(): boolean {
451
+ const accept = this.header('accept', '');
452
+ return accept !== undefined && accept.includes('text/html');
453
+ }
454
+
455
+ // ==========================================
456
+ // Cookies
457
+ // ==========================================
458
+
459
+ /**
460
+ * Get all cookies
461
+ */
462
+ cookies(): Record<string, string> {
463
+ if (this.parsedCookies === null) {
464
+ this.parsedCookies = this.parseCookieHeader();
465
+ }
466
+ return { ...this.parsedCookies };
467
+ }
468
+
469
+ /**
470
+ * Get a specific cookie
471
+ */
472
+ cookie(name: string, defaultValue?: string): string | undefined {
473
+ const cookies = this.cookies();
474
+ return cookies[name] ?? defaultValue;
475
+ }
476
+
477
+ /**
478
+ * Check if a cookie exists
479
+ */
480
+ hasCookie(name: string): boolean {
481
+ return this.cookie(name) !== undefined;
482
+ }
483
+
484
+ /**
485
+ * Parse the Cookie header
486
+ */
487
+ private parseCookieHeader(): Record<string, string> {
488
+ const cookieHeader = this.header('cookie');
489
+ if (!cookieHeader) {
490
+ return {};
491
+ }
492
+
493
+ const cookies: Record<string, string> = {};
494
+ const pairs = cookieHeader.split(';');
495
+
496
+ for (const pair of pairs) {
497
+ const [name, ...valueParts] = pair.split('=');
498
+ if (name) {
499
+ const trimmedName = name.trim();
500
+ const value = valueParts.join('=').trim();
501
+ // Decode URI encoded values
502
+ try {
503
+ cookies[trimmedName] = decodeURIComponent(value);
504
+ } catch {
505
+ cookies[trimmedName] = value;
506
+ }
507
+ }
508
+ }
509
+
510
+ return cookies;
511
+ }
512
+
513
+ // ==========================================
514
+ // File Uploads
515
+ // ==========================================
516
+
517
+ /**
518
+ * Get an uploaded file by name
519
+ */
520
+ async file(name: string): Promise<UploadedFile | null> {
521
+ const files = await this.files();
522
+ const fileList = files.get(name);
523
+ return fileList && fileList.length > 0 ? fileList[0] : null;
524
+ }
525
+
526
+ /**
527
+ * Get all uploaded files
528
+ */
529
+ async files(): Promise<Map<string, UploadedFile[]>> {
530
+ if (this.parsedFiles !== null) {
531
+ return this.parsedFiles;
532
+ }
533
+
534
+ this.parsedFiles = new Map();
535
+
536
+ if (!this.isFormData()) {
537
+ return this.parsedFiles;
538
+ }
539
+
540
+ try {
541
+ const formData = await this.request.formData();
542
+
543
+ for (const [key, value] of formData.entries()) {
544
+ if (value instanceof File) {
545
+ const arrayBuffer = await value.arrayBuffer();
546
+ const uploadedFile = new UploadedFile({
547
+ name: key,
548
+ originalName: value.name,
549
+ mimeType: value.type || 'application/octet-stream',
550
+ size: value.size,
551
+ content: arrayBuffer,
552
+ });
553
+
554
+ const existing = this.parsedFiles.get(key) ?? [];
555
+ existing.push(uploadedFile);
556
+ this.parsedFiles.set(key, existing);
557
+ }
558
+ }
559
+ } catch {
560
+ // If formData parsing fails, return empty map
561
+ }
562
+
563
+ return this.parsedFiles;
564
+ }
565
+
566
+ /**
567
+ * Check if request has an uploaded file
568
+ */
569
+ async hasFile(name: string): Promise<boolean> {
570
+ const file = await this.file(name);
571
+ return file !== null;
572
+ }
573
+
574
+ // ==========================================
575
+ // Request Fingerprinting
576
+ // ==========================================
577
+
578
+ /**
579
+ * Generate a unique fingerprint for the request
580
+ */
581
+ fingerprint(): string {
582
+ const components = [
583
+ this.method(),
584
+ this.path(),
585
+ this.ip() ?? 'unknown',
586
+ this.userAgent() ?? 'unknown',
587
+ ];
588
+ return components.join('|');
589
+ }
590
+
591
+ // ==========================================
592
+ // AJAX / API Detection
593
+ // ==========================================
594
+
595
+ /**
596
+ * Check if request is an AJAX/XHR request
597
+ */
598
+ ajax(): boolean {
599
+ return this.header('x-requested-with')?.toLowerCase() === 'xmlhttprequest';
600
+ }
601
+
602
+ /**
603
+ * Check if request is a preflight request
604
+ */
605
+ isPreflight(): boolean {
606
+ return this.isMethod('OPTIONS') && this.hasHeader('access-control-request-method');
607
+ }
608
+ }