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,297 @@
1
+ /**
2
+ * PhoenixJS - HTTP Kernel
3
+ *
4
+ * The HTTP Kernel is the heart of the framework.
5
+ * It receives all HTTP requests and returns responses.
6
+ * Now integrated with Router and Middleware Pipeline.
7
+ */
8
+
9
+ import { Application } from '@framework/core/Application';
10
+ import { FrameworkRequest } from '@framework/http/Request';
11
+ import { FrameworkResponse } from '@framework/http/Response';
12
+ import { Router } from '@framework/routing/Router';
13
+ import { Pipeline } from '@framework/middleware/Pipeline';
14
+ import type { MiddlewareHandler, MiddlewareResolvable } from '@framework/middleware/Middleware';
15
+ import type { RouteMatch } from '@framework/routing/RouteRegistry';
16
+ import type { ControllerConstructor } from '@framework/controller/Controller';
17
+
18
+ export class Kernel {
19
+ protected app: Application;
20
+ protected globalMiddleware: MiddlewareHandler[] = [];
21
+ protected middlewareGroups: Map<string, MiddlewareHandler[]> = new Map();
22
+
23
+ constructor(app: Application) {
24
+ this.app = app;
25
+ }
26
+
27
+ /**
28
+ * Register global middleware that runs on every request
29
+ */
30
+ middleware(middleware: MiddlewareHandler[]): this {
31
+ this.globalMiddleware = middleware;
32
+ return this;
33
+ }
34
+
35
+ /**
36
+ * Register middleware groups
37
+ */
38
+ middlewareGroup(name: string, middleware: MiddlewareHandler[]): this {
39
+ this.middlewareGroups.set(name, middleware);
40
+ return this;
41
+ }
42
+
43
+ /**
44
+ * Handle an incoming HTTP request
45
+ *
46
+ * This is the main entry point for all HTTP requests.
47
+ * It integrates with the Router and Middleware Pipeline.
48
+ */
49
+ async handle(request: Request): Promise<Response> {
50
+ const frameworkRequest = new FrameworkRequest(request);
51
+
52
+ try {
53
+ // Run the request through global middleware, then dispatch
54
+ const response = await new Pipeline()
55
+ .send(frameworkRequest)
56
+ .through(this.globalMiddleware)
57
+ .then((req) => this.dispatch(req));
58
+
59
+ return response;
60
+ } catch (error) {
61
+ return this.handleException(error);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Dispatch the request through the router
67
+ */
68
+ protected async dispatch(request: FrameworkRequest): Promise<Response> {
69
+ const method = request.method();
70
+ const path = request.path();
71
+
72
+ // Try to resolve the route
73
+ const match = Router.resolve(method, path);
74
+
75
+ if (match) {
76
+ return this.runRoute(request, match);
77
+ }
78
+
79
+ // No route matched - check for fallback routes (root and health)
80
+ return this.handleFallback(request, path);
81
+ }
82
+
83
+ /**
84
+ * Run the matched route through its middleware and handler
85
+ */
86
+ protected async runRoute(request: FrameworkRequest, match: RouteMatch): Promise<Response> {
87
+ const { route, params } = match;
88
+
89
+ // Set the params on the request
90
+ request.setParams(params);
91
+
92
+ // Resolve route middleware
93
+ const routeMiddleware = this.resolveMiddleware(route.middleware);
94
+
95
+ // Run through route middleware then execute handler
96
+ return new Pipeline()
97
+ .send(request)
98
+ .through(routeMiddleware)
99
+ .then(async (req) => {
100
+ return this.executeHandler(req, match);
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Execute the route handler
106
+ */
107
+ protected async executeHandler(request: FrameworkRequest, match: RouteMatch): Promise<Response> {
108
+ const { route, params } = match;
109
+ const handler = route.handler;
110
+
111
+ // If handler is a function, call it directly
112
+ if (typeof handler === 'function') {
113
+ const result = await handler(params);
114
+ return result;
115
+ }
116
+
117
+ // If handler is a string (Controller@method), resolve and execute
118
+ return this.executeControllerAction(request, handler, params);
119
+ }
120
+
121
+ /**
122
+ * Execute a controller action from a string handler
123
+ * Format: "path/to/Controller@methodName" or "ControllerName@methodName"
124
+ */
125
+ protected async executeControllerAction(
126
+ request: FrameworkRequest,
127
+ handler: string,
128
+ params: Record<string, string>
129
+ ): Promise<Response> {
130
+ const atIndex = handler.indexOf('@');
131
+ if (atIndex === -1) {
132
+ return FrameworkResponse.error(
133
+ `Invalid controller handler format: ${handler}. Expected "Controller@method"`,
134
+ 500
135
+ );
136
+ }
137
+
138
+ const controllerPath = handler.substring(0, atIndex);
139
+ const methodName = handler.substring(atIndex + 1);
140
+
141
+ try {
142
+ // Resolve the controller class
143
+ const ControllerClass = await this.resolveController(controllerPath);
144
+
145
+ if (!ControllerClass) {
146
+ return FrameworkResponse.error(
147
+ `Controller not found: ${controllerPath}`,
148
+ 500
149
+ );
150
+ }
151
+
152
+ // Instantiate the controller
153
+ const controller = new ControllerClass();
154
+
155
+ // Inject request and params
156
+ controller.setRequest(request);
157
+ controller.setParams(params);
158
+
159
+ // Check if method exists
160
+ const controllerAny = controller as unknown as Record<string, unknown>;
161
+ if (typeof controllerAny[methodName] !== 'function') {
162
+ console.log(`Debug: Method ${methodName} lookup failed on ${ControllerClass.name}`);
163
+ console.log('Debug: Available keys:', Object.keys(controllerAny));
164
+ console.log('Debug: Prototype keys:', Object.getOwnPropertyNames(Object.getPrototypeOf(controllerAny)));
165
+ // Add more debug info
166
+ console.log('Debug: ControllerClass source:', ControllerClass.toString());
167
+
168
+
169
+ return FrameworkResponse.error(
170
+ `Method ${methodName} not found on controller ${controllerPath}`,
171
+ 500
172
+ );
173
+ }
174
+
175
+ // Execute the method
176
+ const result = await (controllerAny[methodName] as () => Promise<Response>)();
177
+ return result;
178
+ } catch (error) {
179
+ const message = error instanceof Error ? error.message : 'Controller execution failed';
180
+ return FrameworkResponse.error(message, 500);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Resolve a controller class from its path
186
+ * Supports: "user/UserController", "HealthController", full paths
187
+ */
188
+ protected async resolveController(controllerPath: string): Promise<ControllerConstructor | null> {
189
+ // Build possible import paths
190
+ const paths = [
191
+ `@app/controllers/${controllerPath}`,
192
+ `./app/controllers/${controllerPath}`,
193
+ `../app/controllers/${controllerPath}`,
194
+ ];
195
+
196
+ // Normalize path - remove .ts extension if present
197
+ const normalizedPath = controllerPath.replace(/\.ts$/, '');
198
+
199
+ for (const basePath of paths) {
200
+ const fullPath = basePath.replace(controllerPath, normalizedPath);
201
+ try {
202
+ // Dynamic import
203
+ const module = await import(fullPath);
204
+
205
+ // Get the controller class (default export or named export matching filename)
206
+ const className = normalizedPath.split('/').pop() || normalizedPath;
207
+ const ControllerClass = module.default || module[className];
208
+
209
+ if (ControllerClass) {
210
+ return ControllerClass as ControllerConstructor;
211
+ }
212
+ } catch {
213
+ // Try next path
214
+ continue;
215
+ }
216
+ }
217
+
218
+ return null;
219
+ }
220
+
221
+ /**
222
+ * Resolve middleware from their resolvable forms
223
+ */
224
+ protected resolveMiddleware(middleware: MiddlewareResolvable[]): MiddlewareHandler[] {
225
+ const resolved: MiddlewareHandler[] = [];
226
+
227
+ for (const m of middleware) {
228
+ if (typeof m === 'string') {
229
+ // Check for middleware alias
230
+ const aliased = Router.resolveMiddleware(m);
231
+ if (aliased && typeof aliased !== 'string') {
232
+ resolved.push(aliased);
233
+ }
234
+ // Check for middleware group
235
+ else if (this.middlewareGroups.has(m)) {
236
+ resolved.push(...(this.middlewareGroups.get(m) || []));
237
+ }
238
+ // String middleware not resolved - skip for now (will be resolved via controller in Phase 2)
239
+ } else {
240
+ resolved.push(m);
241
+ }
242
+ }
243
+
244
+ return resolved;
245
+ }
246
+
247
+ /**
248
+ * Handle fallback routes (built-in routes like / and /health)
249
+ */
250
+ protected handleFallback(request: FrameworkRequest, path: string): Response {
251
+ // Built-in routes for framework status
252
+ if (path === '/') {
253
+ return FrameworkResponse.json({
254
+ framework: 'PhoenixJS',
255
+ version: '0.1.0',
256
+ message: 'Welcome to PhoenixJS!',
257
+ status: 'running',
258
+ routes: Router.count(),
259
+ });
260
+ }
261
+
262
+ if (path === '/health') {
263
+ return FrameworkResponse.json({
264
+ status: 'healthy',
265
+ timestamp: new Date().toISOString(),
266
+ uptime: process.uptime(),
267
+ });
268
+ }
269
+
270
+ // 404 for unmatched routes
271
+ return FrameworkResponse.notFound(`Route ${path} not found`);
272
+ }
273
+
274
+ /**
275
+ * Handle exceptions
276
+ */
277
+ protected handleException(error: unknown): Response {
278
+ const message = error instanceof Error ? error.message : 'Internal Server Error';
279
+ const stack = this.app.isDebug() && error instanceof Error ? error.stack : undefined;
280
+
281
+ return FrameworkResponse.json(
282
+ {
283
+ error: 'Server Error',
284
+ message,
285
+ ...(stack && { stack }),
286
+ },
287
+ 500
288
+ );
289
+ }
290
+
291
+ /**
292
+ * Get the application instance
293
+ */
294
+ getApplication(): Application {
295
+ return this.app;
296
+ }
297
+ }
@@ -0,0 +1,18 @@
1
+ export interface DatabaseAdapter {
2
+ /**
3
+ * Connect to the database.
4
+ * @param config Optional configuration for the connection
5
+ */
6
+ connect(config?: any): Promise<void>;
7
+
8
+ /**
9
+ * Disconnect from the database.
10
+ */
11
+ disconnect(): Promise<void>;
12
+
13
+ /**
14
+ * Execute a query or operation against the database.
15
+ * @param query Query string, object, or callback depending on the adapter
16
+ */
17
+ query<T>(query: any): Promise<T>;
18
+ }
@@ -0,0 +1,65 @@
1
+ import { DatabaseAdapter } from './DatabaseAdapter';
2
+
3
+ export class PrismaAdapter implements DatabaseAdapter {
4
+ protected client: unknown;
5
+
6
+ constructor(client: unknown) {
7
+ this.client = client;
8
+ }
9
+
10
+ /**
11
+ * Connect to the database via Prisma.
12
+ */
13
+ async connect(): Promise<void> {
14
+ await (this.client as { $connect: () => Promise<void> }).$connect();
15
+ }
16
+
17
+ /**
18
+ * Disconnect from the database.
19
+ */
20
+ async disconnect(): Promise<void> {
21
+ await (this.client as { $disconnect: () => Promise<void> }).$disconnect();
22
+ }
23
+
24
+ /**
25
+ * Execute a query using the Prisma client via callback.
26
+ * The callback receives the full Prisma client for type-safe queries.
27
+ */
28
+ async query<T>(cb: (db: unknown) => Promise<T>): Promise<T> {
29
+ return cb(this.client);
30
+ }
31
+
32
+ /**
33
+ * Execute operations within a transaction.
34
+ * Uses Prisma's interactive transactions for atomicity.
35
+ */
36
+ async transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T> {
37
+ const prismaClient = this.client as { $transaction: <R>(fn: (tx: unknown) => Promise<R>) => Promise<R> };
38
+ return prismaClient.$transaction(fn);
39
+ }
40
+
41
+ /**
42
+ * Execute raw SQL query via Prisma's $queryRaw.
43
+ * Returns the query results.
44
+ */
45
+ async raw<T>(sql: string, ...params: unknown[]): Promise<T> {
46
+ const prismaClient = this.client as { $queryRawUnsafe: <R>(sql: string, ...params: unknown[]) => Promise<R> };
47
+ return prismaClient.$queryRawUnsafe<T>(sql, ...params);
48
+ }
49
+
50
+ /**
51
+ * Execute raw SQL statement (INSERT, UPDATE, DELETE) via Prisma's $executeRaw.
52
+ * Returns the number of affected rows.
53
+ */
54
+ async execute(sql: string, ...params: unknown[]): Promise<number> {
55
+ const prismaClient = this.client as { $executeRawUnsafe: (sql: string, ...params: unknown[]) => Promise<number> };
56
+ return prismaClient.$executeRawUnsafe(sql, ...params);
57
+ }
58
+
59
+ /**
60
+ * Get the underlying Prisma client.
61
+ */
62
+ getClient(): unknown {
63
+ return this.client;
64
+ }
65
+ }
@@ -0,0 +1,117 @@
1
+ import { Database, type SQLQueryBindings } from 'bun:sqlite';
2
+ import { DatabaseAdapter } from './DatabaseAdapter';
3
+
4
+ export interface SqlAdapterConfig {
5
+ filename: string;
6
+ readonly?: boolean;
7
+ create?: boolean;
8
+ }
9
+
10
+ export class SqlAdapter implements DatabaseAdapter {
11
+ protected db: Database | null = null;
12
+ protected config: SqlAdapterConfig;
13
+
14
+ constructor(config: SqlAdapterConfig = { filename: ':memory:' }) {
15
+ this.config = config;
16
+ }
17
+
18
+ /**
19
+ * Connect to the SQLite database.
20
+ */
21
+ async connect(): Promise<void> {
22
+ this.db = new Database(this.config.filename, {
23
+ readonly: this.config.readonly ?? false,
24
+ create: this.config.create ?? true,
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Disconnect from the database.
30
+ */
31
+ async disconnect(): Promise<void> {
32
+ if (this.db) {
33
+ this.db.close();
34
+ this.db = null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Execute a query and return results.
40
+ * For SELECT queries, returns the result rows.
41
+ */
42
+ async query<T>(sql: string, params?: SQLQueryBindings[]): Promise<T> {
43
+ this.ensureConnected();
44
+ const stmt = this.db!.prepare(sql);
45
+ if (params && params.length > 0) {
46
+ return stmt.all(...params) as T;
47
+ }
48
+ return stmt.all() as T;
49
+ }
50
+
51
+ /**
52
+ * Execute a statement (INSERT, UPDATE, DELETE) without returning rows.
53
+ * Returns the number of changes made.
54
+ */
55
+ async execute(sql: string, params?: SQLQueryBindings[]): Promise<number> {
56
+ this.ensureConnected();
57
+ const stmt = this.db!.prepare(sql);
58
+ let result;
59
+ if (params && params.length > 0) {
60
+ result = stmt.run(...params);
61
+ } else {
62
+ result = stmt.run();
63
+ }
64
+ return result.changes;
65
+ }
66
+
67
+ /**
68
+ * Execute multiple statements in a transaction.
69
+ */
70
+ async transaction<T>(fn: () => Promise<T>): Promise<T> {
71
+ this.ensureConnected();
72
+
73
+ const db = this.db!;
74
+ db.run('BEGIN TRANSACTION');
75
+
76
+ try {
77
+ const result = await fn();
78
+ db.run('COMMIT');
79
+ return result;
80
+ } catch (error) {
81
+ db.run('ROLLBACK');
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Get a single row from a query.
88
+ */
89
+ async get<T>(sql: string, params?: SQLQueryBindings[]): Promise<T | null> {
90
+ this.ensureConnected();
91
+ const stmt = this.db!.prepare(sql);
92
+ if (params && params.length > 0) {
93
+ return (stmt.get(...params) as T) ?? null;
94
+ }
95
+ return (stmt.get() as T) ?? null;
96
+ }
97
+
98
+ /**
99
+ * Check if the adapter is connected.
100
+ */
101
+ isConnected(): boolean {
102
+ return this.db !== null;
103
+ }
104
+
105
+ /**
106
+ * Get the underlying Database instance.
107
+ */
108
+ getDatabase(): Database | null {
109
+ return this.db;
110
+ }
111
+
112
+ private ensureConnected(): void {
113
+ if (!this.db) {
114
+ throw new Error('SqlAdapter is not connected. Call connect() first.');
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * PhoenixJS - Gateway
3
+ *
4
+ * Transport-agnostic Gateway interface supporting WebSocket, Socket.IO, and gRPC.
5
+ */
6
+
7
+ import type { Application } from '@framework/core/Application';
8
+ import type { ServerWebSocket } from 'bun';
9
+
10
+ /**
11
+ * Supported transport types
12
+ */
13
+ export type GatewayTransport = 'websocket' | 'socketio' | 'grpc';
14
+
15
+ /**
16
+ * Gateway interface - base contract for all gateways
17
+ */
18
+ export interface Gateway {
19
+ /** Unique gateway name */
20
+ name: string;
21
+
22
+ /** Transport protocol to use */
23
+ transport: GatewayTransport;
24
+
25
+ /** URL path for the gateway (e.g., '/ws', '/chat') */
26
+ path?: string;
27
+
28
+ /** Called when gateway is registered */
29
+ register(app: Application): void;
30
+
31
+ /** Called after all gateways are registered */
32
+ boot?(app: Application): void;
33
+ }
34
+
35
+ /**
36
+ * WebSocket-specific gateway interface
37
+ */
38
+ export interface WebSocketGatewayInterface extends Gateway {
39
+ transport: 'websocket';
40
+
41
+ /** Called when a connection is opened */
42
+ onOpen?(ws: ServerWebSocket<WebSocketData>): void | Promise<void>;
43
+
44
+ /** Called when a message is received */
45
+ onMessage?(ws: ServerWebSocket<WebSocketData>, message: string | Buffer): void | Promise<void>;
46
+
47
+ /** Called when a connection is closed */
48
+ onClose?(ws: ServerWebSocket<WebSocketData>, code: number, reason: string): void | Promise<void>;
49
+
50
+ /** Called when an error occurs */
51
+ onError?(ws: ServerWebSocket<WebSocketData>, error: Error): void | Promise<void>;
52
+
53
+ /** Called when the socket is ready to receive more data */
54
+ onDrain?(ws: ServerWebSocket<WebSocketData>): void | Promise<void>;
55
+ }
56
+
57
+ /**
58
+ * Contextual data attached to WebSocket connections
59
+ */
60
+ export interface WebSocketData {
61
+ /** Gateway handling this connection */
62
+ gateway: string;
63
+
64
+ /** Connection ID */
65
+ connectionId: string;
66
+
67
+ /** Connection timestamp */
68
+ connectedAt: number;
69
+
70
+ /** Custom user data */
71
+ [key: string]: unknown;
72
+ }
73
+
74
+ /**
75
+ * Gateway configuration options
76
+ */
77
+ export interface GatewayOptions {
78
+ /** Path prefix for the gateway */
79
+ path?: string;
80
+
81
+ /** Idle timeout in seconds (default: 120) */
82
+ idleTimeout?: number;
83
+
84
+ /** Max message payload size in bytes (default: 1MB) */
85
+ maxPayloadLength?: number;
86
+
87
+ /** Enable per-message-deflate compression */
88
+ perMessageDeflate?: boolean;
89
+ }
90
+
91
+ /**
92
+ * Transport adapter interface for pluggable transports
93
+ */
94
+ export interface TransportAdapter {
95
+ /** Adapter name */
96
+ name: string;
97
+
98
+ /** Transport type this adapter handles */
99
+ transport: GatewayTransport;
100
+
101
+ /** Start the transport adapter */
102
+ start(gateways: Gateway[]): void;
103
+
104
+ /** Stop the transport adapter */
105
+ stop(): void;
106
+
107
+ /** Check if adapter is running */
108
+ isRunning(): boolean;
109
+ }