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,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
|
+
}
|