filamentjs 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/.ai/instructions.md +44 -0
- package/README.md +273 -0
- package/dist/src/application.d.ts +76 -0
- package/dist/src/application.d.ts.map +1 -0
- package/dist/src/application.js +313 -0
- package/dist/src/application.js.map +1 -0
- package/dist/src/example.d.ts +2 -0
- package/dist/src/example.d.ts.map +1 -0
- package/dist/src/example.js +110 -0
- package/dist/src/example.js.map +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/response.d.ts +18 -0
- package/dist/src/response.d.ts.map +1 -0
- package/dist/src/response.js +46 -0
- package/dist/src/response.js.map +1 -0
- package/dist/src/router.d.ts +18 -0
- package/dist/src/router.d.ts.map +1 -0
- package/dist/src/router.js +35 -0
- package/dist/src/router.js.map +1 -0
- package/dist/src/types.d.ts +47 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tests/application.test.d.ts +2 -0
- package/dist/tests/application.test.d.ts.map +1 -0
- package/dist/tests/application.test.js +390 -0
- package/dist/tests/application.test.js.map +1 -0
- package/dist/tests/response.test.d.ts +2 -0
- package/dist/tests/response.test.d.ts.map +1 -0
- package/dist/tests/response.test.js +224 -0
- package/dist/tests/response.test.js.map +1 -0
- package/dist/tests/router.test.d.ts +2 -0
- package/dist/tests/router.test.d.ts.map +1 -0
- package/dist/tests/router.test.js +101 -0
- package/dist/tests/router.test.js.map +1 -0
- package/examples/01-blog-api.ts +182 -0
- package/examples/02-api-versioning.ts +167 -0
- package/examples/03-performance-controls.ts +240 -0
- package/examples/04-observability.ts +322 -0
- package/examples/05-content-negotiation.ts +268 -0
- package/examples/README.md +274 -0
- package/package.json +29 -0
- package/src/application.ts +390 -0
- package/src/example.ts +154 -0
- package/src/index.ts +16 -0
- package/src/response.ts +56 -0
- package/src/router.ts +47 -0
- package/src/types.ts +85 -0
- package/tests/README.md +56 -0
- package/tests/application.test.ts +493 -0
- package/tests/response.test.ts +255 -0
- package/tests/router.test.ts +111 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Filament AI Development Guidelines
|
|
2
|
+
|
|
3
|
+
## Architecture-specific patterns
|
|
4
|
+
|
|
5
|
+
- **Meta merging**: Shallow merge with defaults - **arrays replace, not merge**
|
|
6
|
+
- **Response flow order**: Middleware → Handler → Transformers → Finalizers
|
|
7
|
+
- Transformers only run on success
|
|
8
|
+
- Finalizers always run (even on errors)
|
|
9
|
+
- **Type pattern**: All classes use `<T extends FrameworkMeta>` generic
|
|
10
|
+
|
|
11
|
+
## Coding standards
|
|
12
|
+
|
|
13
|
+
- Lines under 80 columns when possible
|
|
14
|
+
- Comments are to document design and usage, not to repeat code.
|
|
15
|
+
- When updating code, ensure that the READMEs and test cases are also up to date whith your changes.
|
|
16
|
+
|
|
17
|
+
### Typescript
|
|
18
|
+
|
|
19
|
+
- imports should be in this order: Native, public libraries, local modules. Alphabetize within this order. Local modules may be difficult to alphabetize, just do your best.
|
|
20
|
+
|
|
21
|
+
### Markdown
|
|
22
|
+
|
|
23
|
+
- Markdowns should have an empty line after all headers and before all lists.
|
|
24
|
+
|
|
25
|
+
### Testing (Non-standard tooling)
|
|
26
|
+
|
|
27
|
+
- **Assertions**: `test-battery` library, NOT chai/assert
|
|
28
|
+
- **Test framework**: Node.js native test runner (`node:test`), NOT mocha/jest.
|
|
29
|
+
- Favour the native name `suite`, not its aliases like `describe`. Use
|
|
30
|
+
`test-battery` to handle the `test` part.
|
|
31
|
+
- Integration tests use real HTTP servers on high-numbered ports (9876+)
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { describe, it } from 'node:test';
|
|
35
|
+
import TestBattery from 'test-battery';
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Protected files
|
|
39
|
+
|
|
40
|
+
DO NOT modify: `/dist/*`, `package-lock.json`, `tsconfig.json` (ask first)
|
|
41
|
+
|
|
42
|
+
## Adding route methods
|
|
43
|
+
|
|
44
|
+
Follow pattern in `application.ts:62-81` - must update Route type and add handler
|
package/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Filament
|
|
2
|
+
|
|
3
|
+
A TypeScript API framework with metadata-driven middleware. Similar in purpose to Express but organized around typed endpoint metadata that controls middleware behavior.
|
|
4
|
+
|
|
5
|
+
## Why Filament?
|
|
6
|
+
|
|
7
|
+
### Best Features
|
|
8
|
+
|
|
9
|
+
- **Type-Safe Metadata**: Full TypeScript support means your middleware logic is validated at compile time
|
|
10
|
+
- **Zero Runtime Overhead**: Metadata inspection is fast—no reflection or complex routing logic
|
|
11
|
+
- **Predictable Execution**: Registration order is everything—no magic, no surprises
|
|
12
|
+
- **Immutable Request Context**: Middleware can't accidentally corrupt endpoint metadata
|
|
13
|
+
- **Flexible Post-Processing**: Handle errors, transform responses, and finalize requests with dedicated hooks
|
|
14
|
+
- **Express-Familiar API**: If you know Express, you know Filament—intuitive and approachable
|
|
15
|
+
- **Minimal Dependencies**: Lightweight framework perfect for microservices and APIs
|
|
16
|
+
- **Strongly Typed Middleware**: Know exactly what metadata your middleware needs before writing a single line
|
|
17
|
+
|
|
18
|
+
## Core Concepts
|
|
19
|
+
|
|
20
|
+
### 1. Endpoint Metadata (`EndpointMeta`)
|
|
21
|
+
|
|
22
|
+
Every endpoint has metadata that describes its requirements and behavior. Middleware inspects this metadata to decide whether and how to execute.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
interface AppMeta extends FrameworkMeta {
|
|
26
|
+
requiresAuth: boolean;
|
|
27
|
+
rateLimit: number;
|
|
28
|
+
logLevel: 'debug' | 'info' | 'error';
|
|
29
|
+
tags: string[];
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Default Metadata
|
|
34
|
+
|
|
35
|
+
You define a complete default metadata object when creating your app. Individual endpoints can override specific properties using `Partial<T>`.
|
|
36
|
+
|
|
37
|
+
### 3. Single Middleware Chain
|
|
38
|
+
|
|
39
|
+
Middleware runs in registration order. Each middleware inspects `req.endpointMeta` to decide what to do.
|
|
40
|
+
|
|
41
|
+
### 4. Post-Request Processing
|
|
42
|
+
|
|
43
|
+
Three types of post-request handlers:
|
|
44
|
+
|
|
45
|
+
- **Error Handlers**: Run when errors occur
|
|
46
|
+
- **Response Transformers**: Modify successful responses
|
|
47
|
+
- **Finalizers**: Always run, regardless of success/failure
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { createApp, FrameworkMeta } from 'filamentjs';
|
|
53
|
+
|
|
54
|
+
// Define your metadata interface
|
|
55
|
+
interface AppMeta extends FrameworkMeta {
|
|
56
|
+
requiresAuth: boolean;
|
|
57
|
+
rateLimit: number;
|
|
58
|
+
logLevel: 'debug' | 'info' | 'error';
|
|
59
|
+
tags: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Create default metadata
|
|
63
|
+
const defaultMeta: AppMeta = {
|
|
64
|
+
requiresAuth: false,
|
|
65
|
+
rateLimit: 100,
|
|
66
|
+
logLevel: 'info',
|
|
67
|
+
tags: [],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Create app
|
|
71
|
+
const app = createApp<AppMeta>(defaultMeta);
|
|
72
|
+
|
|
73
|
+
// Add middleware that inspects metadata
|
|
74
|
+
app.use(async (req, res, next) => {
|
|
75
|
+
if (req.endpointMeta.requiresAuth) {
|
|
76
|
+
const token = req.headers.authorization;
|
|
77
|
+
if (!token) {
|
|
78
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// validate token...
|
|
82
|
+
}
|
|
83
|
+
await next();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Define endpoints with custom metadata
|
|
87
|
+
app.get('/public', {}, async (req, res) => {
|
|
88
|
+
res.json({ message: 'Public endpoint' });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
app.get('/admin',
|
|
92
|
+
{ requiresAuth: true, logLevel: 'debug' },
|
|
93
|
+
async (req, res) => {
|
|
94
|
+
res.json({ message: 'Admin panel' });
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Start server
|
|
99
|
+
app.listen(3000);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## API Reference
|
|
103
|
+
|
|
104
|
+
### `createApp<T>(defaultMeta: T): Application<T>`
|
|
105
|
+
|
|
106
|
+
Creates a new application instance with typed metadata.
|
|
107
|
+
|
|
108
|
+
**Parameters:**
|
|
109
|
+
|
|
110
|
+
- `defaultMeta`: Complete implementation of your metadata interface
|
|
111
|
+
|
|
112
|
+
**Returns:** Application instance
|
|
113
|
+
|
|
114
|
+
### Application Methods
|
|
115
|
+
|
|
116
|
+
#### HTTP Methods
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
app.get(path: string, meta: Partial<T>, handler: AsyncRequestHandler): void
|
|
120
|
+
app.post(path: string, meta: Partial<T>, handler: AsyncRequestHandler): void
|
|
121
|
+
app.put(path: string, meta: Partial<T>, handler: AsyncRequestHandler): void
|
|
122
|
+
app.patch(path: string, meta: Partial<T>, handler: AsyncRequestHandler): void
|
|
123
|
+
app.delete(path: string, meta: Partial<T>, handler: AsyncRequestHandler): void
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### Middleware Registration
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
app.use(middleware: AsyncRequestHandler): void
|
|
130
|
+
app.use(path: string, middleware: AsyncRequestHandler): void
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### Post-Request Handlers
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
app.onError(handler: ErrorHandler<T>): void
|
|
137
|
+
app.onTransform(handler: ResponseTransformer<T>): void
|
|
138
|
+
app.onFinalize(handler: Finalizer<T>): void
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### Server Control
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
app.listen(port: number, callback?: () => void): void
|
|
145
|
+
app.close(): Promise<void>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Request Object
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
interface Request<T extends FrameworkMeta> {
|
|
152
|
+
method: HttpMethod;
|
|
153
|
+
path: string;
|
|
154
|
+
params: Record<string, string>; // Path parameters
|
|
155
|
+
query: Record<string, string | string[]>; // Query parameters
|
|
156
|
+
headers: Record<string, string | string[] | undefined>;
|
|
157
|
+
body?: unknown; // Parsed JSON body
|
|
158
|
+
endpointMeta: Readonly<T>; // Endpoint metadata (read-only)
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Response Object
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
interface Response {
|
|
166
|
+
status(code: number): Response;
|
|
167
|
+
setHeader(name: string, value: string | string[]): Response;
|
|
168
|
+
json(data: unknown): void;
|
|
169
|
+
send(data: string | Buffer): void;
|
|
170
|
+
end(): void;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Request Lifecycle
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
1. Incoming Request
|
|
178
|
+
↓
|
|
179
|
+
2. Route Matching → req.endpointMeta populated (readonly)
|
|
180
|
+
↓
|
|
181
|
+
3. Middleware Chain (in registration order)
|
|
182
|
+
- Each middleware inspects req.endpointMeta
|
|
183
|
+
- Decides whether to execute logic
|
|
184
|
+
- await next() continues chain
|
|
185
|
+
↓
|
|
186
|
+
4. Route Handler executes
|
|
187
|
+
↓
|
|
188
|
+
5. [If Success] Response Transformers (sequential, awaited)
|
|
189
|
+
↓
|
|
190
|
+
6. [If Error anywhere] Error Handlers (sequential, awaited)
|
|
191
|
+
↓
|
|
192
|
+
7. Finalizers (always run, sequential, awaited)
|
|
193
|
+
↓
|
|
194
|
+
8. Response sent
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Path Parameters
|
|
198
|
+
|
|
199
|
+
Supports Express-style path parameters:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
app.get('/users/:id', {}, async (req, res) => {
|
|
203
|
+
const userId = req.params.id; // string
|
|
204
|
+
res.json({ userId });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
app.get('/posts/:postId/comments/:commentId', {}, async (req, res) => {
|
|
208
|
+
const { postId, commentId } = req.params;
|
|
209
|
+
res.json({ postId, commentId });
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Metadata Merging
|
|
214
|
+
|
|
215
|
+
- Endpoint metadata is merged with defaults using shallow merge
|
|
216
|
+
- Arrays **always replace** (not concatenate)
|
|
217
|
+
- Metadata is immutable at runtime
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const defaultMeta = {
|
|
221
|
+
requiresAuth: false,
|
|
222
|
+
tags: ['default'],
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
app.get('/endpoint',
|
|
226
|
+
{ requiresAuth: true, tags: ['custom'] }, // tags replaces, not appends
|
|
227
|
+
handler
|
|
228
|
+
);
|
|
229
|
+
// Result: { requiresAuth: true, tags: ['custom'] }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Error Handling
|
|
233
|
+
|
|
234
|
+
Errors thrown anywhere in the request lifecycle are caught and passed to error handlers:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
app.onError(async (err, req, res) => {
|
|
238
|
+
console.error('Error:', err);
|
|
239
|
+
res.status(500).json({ error: err.message });
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Complete Example
|
|
244
|
+
|
|
245
|
+
See `src/example.ts` for a complete working example with:
|
|
246
|
+
|
|
247
|
+
- Authentication middleware
|
|
248
|
+
- Rate limiting middleware
|
|
249
|
+
- Logging middleware
|
|
250
|
+
- Multiple endpoints with different metadata
|
|
251
|
+
- Response transformers
|
|
252
|
+
- Error handling
|
|
253
|
+
- Finalizers
|
|
254
|
+
|
|
255
|
+
## Design Decisions
|
|
256
|
+
|
|
257
|
+
1. **Immutable Metadata**: `req.endpointMeta` is read-only to prevent middleware from creating hidden dependencies
|
|
258
|
+
2. **Async by Default**: All handlers support `async/await`
|
|
259
|
+
3. **Registration Order**: Middleware runs in strict registration order
|
|
260
|
+
4. **Path Parameters**: Typed as `Record<string, string>` (no advanced type inference)
|
|
261
|
+
5. **Array Replacement**: Arrays in metadata always replace (never merge)
|
|
262
|
+
|
|
263
|
+
## TypeScript
|
|
264
|
+
|
|
265
|
+
Full TypeScript support with strict typing:
|
|
266
|
+
|
|
267
|
+
- Generic `Application<T>` for typed metadata
|
|
268
|
+
- Type-safe request/response objects
|
|
269
|
+
- Compile-time validation of metadata interfaces
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
ISC
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { FrameworkMeta, AsyncRequestHandler, ErrorHandler, Finalizer, ResponseTransformer } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Main Application class for Filament
|
|
4
|
+
*/
|
|
5
|
+
export declare class Application<T extends FrameworkMeta> {
|
|
6
|
+
private routes;
|
|
7
|
+
private middlewares;
|
|
8
|
+
private errorHandlers;
|
|
9
|
+
private finalizers;
|
|
10
|
+
private transformers;
|
|
11
|
+
private defaultMeta;
|
|
12
|
+
private server?;
|
|
13
|
+
constructor(defaultMeta: T);
|
|
14
|
+
/**
|
|
15
|
+
* Register a route
|
|
16
|
+
*/
|
|
17
|
+
private route;
|
|
18
|
+
/**
|
|
19
|
+
* Merge partial meta with defaults
|
|
20
|
+
*/
|
|
21
|
+
private mergeMeta;
|
|
22
|
+
get(path: string, meta: Partial<T>, handler: AsyncRequestHandler<T>): void;
|
|
23
|
+
post(path: string, meta: Partial<T>, handler: AsyncRequestHandler<T>): void;
|
|
24
|
+
put(path: string, meta: Partial<T>, handler: AsyncRequestHandler<T>): void;
|
|
25
|
+
patch(path: string, meta: Partial<T>, handler: AsyncRequestHandler<T>): void;
|
|
26
|
+
delete(path: string, meta: Partial<T>, handler: AsyncRequestHandler<T>): void;
|
|
27
|
+
/**
|
|
28
|
+
* Register middleware
|
|
29
|
+
*/
|
|
30
|
+
use(pathOrHandler: string | AsyncRequestHandler<T>, handler?: AsyncRequestHandler<T>): void;
|
|
31
|
+
/**
|
|
32
|
+
* Register error handler
|
|
33
|
+
*/
|
|
34
|
+
onError(handler: ErrorHandler<T>): void;
|
|
35
|
+
/**
|
|
36
|
+
* Register finalizer
|
|
37
|
+
*/
|
|
38
|
+
onFinalize(handler: Finalizer<T>): void;
|
|
39
|
+
/**
|
|
40
|
+
* Register response transformer
|
|
41
|
+
*/
|
|
42
|
+
onTransform(handler: ResponseTransformer<T>): void;
|
|
43
|
+
/**
|
|
44
|
+
* Execute middleware chain
|
|
45
|
+
*/
|
|
46
|
+
private executeMiddlewareChain;
|
|
47
|
+
/**
|
|
48
|
+
* Execute error handlers
|
|
49
|
+
*/
|
|
50
|
+
private executeErrorHandlers;
|
|
51
|
+
/**
|
|
52
|
+
* Execute response transformers
|
|
53
|
+
*/
|
|
54
|
+
private executeTransformers;
|
|
55
|
+
/**
|
|
56
|
+
* Execute finalizers
|
|
57
|
+
*/
|
|
58
|
+
private executeFinalizers;
|
|
59
|
+
/**
|
|
60
|
+
* Handle incoming HTTP request
|
|
61
|
+
*/
|
|
62
|
+
private handleRequest;
|
|
63
|
+
/**
|
|
64
|
+
* Start the server
|
|
65
|
+
*/
|
|
66
|
+
listen(port: number): Promise<number>;
|
|
67
|
+
/**
|
|
68
|
+
* Stop the server
|
|
69
|
+
*/
|
|
70
|
+
close(): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Factory function to create an application
|
|
74
|
+
*/
|
|
75
|
+
export declare function createApp<T extends FrameworkMeta>(defaultMeta: T): Application<T>;
|
|
76
|
+
//# sourceMappingURL=application.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../../src/application.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,aAAa,EAIb,mBAAmB,EACnB,YAAY,EACZ,SAAS,EACT,mBAAmB,EAGpB,MAAM,YAAY,CAAC;AAIpB;;GAEG;AACH,qBAAa,WAAW,CAAC,CAAC,SAAS,aAAa;IAC9C,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,WAAW,CAAI;IACvB,OAAO,CAAC,MAAM,CAAC,CAAc;gBAEjB,WAAW,EAAE,CAAC;IAI1B;;OAEG;IACH,OAAO,CAAC,KAAK;IAmBb;;OAEG;IACH,OAAO,CAAC,SAAS;IASjB,GAAG,CACD,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAChB,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC9B,IAAI;IAIP,IAAI,CACF,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAChB,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC9B,IAAI;IAIP,GAAG,CACD,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAChB,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC9B,IAAI;IAIP,KAAK,CACH,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAChB,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC9B,IAAI;IAIP,MAAM,CACJ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAChB,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC9B,IAAI;IAIP;;OAEG;IACH,GAAG,CACD,aAAa,EAAE,MAAM,GAAG,mBAAmB,CAAC,CAAC,CAAC,EAC9C,OAAO,CAAC,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC/B,IAAI;IAQP;;OAEG;IACH,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI;IAIvC;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI;IAIvC;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,IAAI;IAIlD;;OAEG;YACW,sBAAsB;IAmBpC;;OAEG;YACW,oBAAoB;IAmBlC;;OAEG;YACW,mBAAmB;IAWjC;;OAEG;YACW,iBAAiB;IAW/B;;OAEG;YACW,aAAa;IAsH3B;;OAEG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA6B3C;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAYvB;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,aAAa,EAAE,WAAW,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAEjF"}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { URL } from 'url';
|
|
3
|
+
import { ResponseImpl } from './response.js';
|
|
4
|
+
import { pathToRegex, matchPath } from './router.js';
|
|
5
|
+
/**
|
|
6
|
+
* Main Application class for Filament
|
|
7
|
+
*/
|
|
8
|
+
export class Application {
|
|
9
|
+
constructor(defaultMeta) {
|
|
10
|
+
this.routes = [];
|
|
11
|
+
this.middlewares = [];
|
|
12
|
+
this.errorHandlers = [];
|
|
13
|
+
this.finalizers = [];
|
|
14
|
+
this.transformers = [];
|
|
15
|
+
this.defaultMeta = defaultMeta;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Register a route
|
|
19
|
+
*/
|
|
20
|
+
route(method, path, meta, handler) {
|
|
21
|
+
const { pattern, paramNames } = pathToRegex(path);
|
|
22
|
+
const mergedMeta = this.mergeMeta(meta);
|
|
23
|
+
this.routes.push({
|
|
24
|
+
method,
|
|
25
|
+
path,
|
|
26
|
+
pattern,
|
|
27
|
+
paramNames,
|
|
28
|
+
meta: mergedMeta,
|
|
29
|
+
handler,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Merge partial meta with defaults
|
|
34
|
+
*/
|
|
35
|
+
mergeMeta(partial) {
|
|
36
|
+
// Shallow merge - arrays replace
|
|
37
|
+
return {
|
|
38
|
+
...this.defaultMeta,
|
|
39
|
+
...partial,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// HTTP method helpers
|
|
43
|
+
get(path, meta, handler) {
|
|
44
|
+
this.route('GET', path, meta, handler);
|
|
45
|
+
}
|
|
46
|
+
post(path, meta, handler) {
|
|
47
|
+
this.route('POST', path, meta, handler);
|
|
48
|
+
}
|
|
49
|
+
put(path, meta, handler) {
|
|
50
|
+
this.route('PUT', path, meta, handler);
|
|
51
|
+
}
|
|
52
|
+
patch(path, meta, handler) {
|
|
53
|
+
this.route('PATCH', path, meta, handler);
|
|
54
|
+
}
|
|
55
|
+
delete(path, meta, handler) {
|
|
56
|
+
this.route('DELETE', path, meta, handler);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Register middleware
|
|
60
|
+
*/
|
|
61
|
+
use(pathOrHandler, handler) {
|
|
62
|
+
if (typeof pathOrHandler === 'string' && handler) {
|
|
63
|
+
this.middlewares.push({ path: pathOrHandler, handler });
|
|
64
|
+
}
|
|
65
|
+
else if (typeof pathOrHandler === 'function') {
|
|
66
|
+
this.middlewares.push({ handler: pathOrHandler });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Register error handler
|
|
71
|
+
*/
|
|
72
|
+
onError(handler) {
|
|
73
|
+
this.errorHandlers.push(handler);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Register finalizer
|
|
77
|
+
*/
|
|
78
|
+
onFinalize(handler) {
|
|
79
|
+
this.finalizers.push(handler);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Register response transformer
|
|
83
|
+
*/
|
|
84
|
+
onTransform(handler) {
|
|
85
|
+
this.transformers.push(handler);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Execute middleware chain
|
|
89
|
+
*/
|
|
90
|
+
async executeMiddlewareChain(req, res, middlewares) {
|
|
91
|
+
let currentIndex = 0;
|
|
92
|
+
const next = async () => {
|
|
93
|
+
if (currentIndex >= middlewares.length) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const middleware = middlewares[currentIndex++];
|
|
97
|
+
await middleware(req, res, next);
|
|
98
|
+
};
|
|
99
|
+
await next();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Execute error handlers
|
|
103
|
+
*/
|
|
104
|
+
async executeErrorHandlers(err, req, res) {
|
|
105
|
+
for (const handler of this.errorHandlers) {
|
|
106
|
+
try {
|
|
107
|
+
await handler(err, req, res, async () => { });
|
|
108
|
+
}
|
|
109
|
+
catch (handlerError) {
|
|
110
|
+
console.error('Error in error handler:', handlerError);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// If no error handler sent a response, send default error
|
|
114
|
+
if (!res.headersSent) {
|
|
115
|
+
res.status(500).json({ error: 'Internal Server Error' });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Execute response transformers
|
|
120
|
+
*/
|
|
121
|
+
async executeTransformers(req, res) {
|
|
122
|
+
for (const transformer of this.transformers) {
|
|
123
|
+
try {
|
|
124
|
+
await transformer(req, res);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.error('Error in response transformer:', err);
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Execute finalizers
|
|
134
|
+
*/
|
|
135
|
+
async executeFinalizers(req, res) {
|
|
136
|
+
for (const finalizer of this.finalizers) {
|
|
137
|
+
try {
|
|
138
|
+
await finalizer(req, res);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.error('Error in finalizer:', err);
|
|
142
|
+
// Finalizers should not block response
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Handle incoming HTTP request
|
|
148
|
+
*/
|
|
149
|
+
async handleRequest(nodeReq, nodeRes) {
|
|
150
|
+
const url = new URL(nodeReq.url || '/', `http://${nodeReq.headers.host || 'localhost'}`);
|
|
151
|
+
const method = (nodeReq.method || 'GET').toUpperCase();
|
|
152
|
+
const path = url.pathname;
|
|
153
|
+
// Find matching route
|
|
154
|
+
let matchedRoute;
|
|
155
|
+
let params = {};
|
|
156
|
+
for (const route of this.routes) {
|
|
157
|
+
if (route.method !== method)
|
|
158
|
+
continue;
|
|
159
|
+
const match = matchPath(path, route.pattern, route.paramNames);
|
|
160
|
+
if (match) {
|
|
161
|
+
matchedRoute = route;
|
|
162
|
+
params = match.params;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!matchedRoute) {
|
|
167
|
+
nodeRes.statusCode = 404;
|
|
168
|
+
nodeRes.end(JSON.stringify({ error: 'Not Found' }));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Parse query parameters
|
|
172
|
+
const query = {};
|
|
173
|
+
url.searchParams.forEach((value, key) => {
|
|
174
|
+
const existing = query[key];
|
|
175
|
+
if (existing) {
|
|
176
|
+
query[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
query[key] = value;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
// Parse headers
|
|
183
|
+
const headers = {};
|
|
184
|
+
Object.entries(nodeReq.headers).forEach(([key, value]) => {
|
|
185
|
+
headers[key] = value;
|
|
186
|
+
});
|
|
187
|
+
// Create request object
|
|
188
|
+
const req = {
|
|
189
|
+
method,
|
|
190
|
+
path,
|
|
191
|
+
params,
|
|
192
|
+
query,
|
|
193
|
+
headers,
|
|
194
|
+
endpointMeta: matchedRoute.meta,
|
|
195
|
+
_startTime: Date.now(),
|
|
196
|
+
};
|
|
197
|
+
// Read body for POST/PUT/PATCH
|
|
198
|
+
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
199
|
+
const chunks = [];
|
|
200
|
+
for await (const chunk of nodeReq) {
|
|
201
|
+
chunks.push(chunk);
|
|
202
|
+
}
|
|
203
|
+
const bodyStr = Buffer.concat(chunks).toString();
|
|
204
|
+
if (bodyStr) {
|
|
205
|
+
try {
|
|
206
|
+
req.body = JSON.parse(bodyStr);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
req.body = bodyStr;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Create response object
|
|
214
|
+
const res = new ResponseImpl((finalRes) => {
|
|
215
|
+
nodeRes.statusCode = finalRes.statusCode;
|
|
216
|
+
Object.entries(finalRes.headers).forEach(([key, value]) => {
|
|
217
|
+
nodeRes.setHeader(key, value);
|
|
218
|
+
});
|
|
219
|
+
if (finalRes.body) {
|
|
220
|
+
nodeRes.end(finalRes.body);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
nodeRes.end();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
let hadError = false;
|
|
227
|
+
try {
|
|
228
|
+
// Filter applicable middleware (by path if specified)
|
|
229
|
+
const applicableMiddleware = this.middlewares
|
|
230
|
+
.filter((mw) => !mw.path || path.startsWith(mw.path))
|
|
231
|
+
.map((mw) => mw.handler);
|
|
232
|
+
// Execute middleware chain
|
|
233
|
+
await this.executeMiddlewareChain(req, res, applicableMiddleware);
|
|
234
|
+
// If response already sent by middleware, skip handler
|
|
235
|
+
if (!res.headersSent) {
|
|
236
|
+
// Execute route handler
|
|
237
|
+
await matchedRoute.handler(req, res, async () => { });
|
|
238
|
+
// Execute response transformers (only on success)
|
|
239
|
+
if (!res.headersSent) {
|
|
240
|
+
await this.executeTransformers(req, res);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
hadError = true;
|
|
246
|
+
await this.executeErrorHandlers(err, req, res);
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
// Always execute finalizers
|
|
250
|
+
await this.executeFinalizers(req, res);
|
|
251
|
+
// Ensure response is sent
|
|
252
|
+
if (!res.headersSent) {
|
|
253
|
+
res.end();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Start the server
|
|
259
|
+
*/
|
|
260
|
+
async listen(port) {
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
this.server = http.createServer((req, res) => {
|
|
263
|
+
this.handleRequest(req, res).catch((err) => {
|
|
264
|
+
console.error('Unhandled error in request handler:', err);
|
|
265
|
+
if (!res.headersSent) {
|
|
266
|
+
res.statusCode = 500;
|
|
267
|
+
res.end(JSON.stringify({ error: 'Internal Server Error' }));
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
this.server.on('error', (err) => {
|
|
272
|
+
if (err.code === 'EADDRINUSE') {
|
|
273
|
+
reject(new Error(`Port ${port} is already in use`));
|
|
274
|
+
}
|
|
275
|
+
else if (err.code === 'EACCES') {
|
|
276
|
+
reject(new Error(`Permission denied: cannot listen on port ${port}`));
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
reject(new Error(`Failed to start server on port ${port}: ${err.message}`));
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
this.server.listen(port, () => {
|
|
283
|
+
console.log(`Server listening on port ${port}`);
|
|
284
|
+
resolve(port);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Stop the server
|
|
290
|
+
*/
|
|
291
|
+
close() {
|
|
292
|
+
return new Promise((resolve, reject) => {
|
|
293
|
+
if (this.server) {
|
|
294
|
+
this.server.close((err) => {
|
|
295
|
+
if (err)
|
|
296
|
+
reject(err);
|
|
297
|
+
else
|
|
298
|
+
resolve();
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
resolve();
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Factory function to create an application
|
|
309
|
+
*/
|
|
310
|
+
export function createApp(defaultMeta) {
|
|
311
|
+
return new Application(defaultMeta);
|
|
312
|
+
}
|
|
313
|
+
//# sourceMappingURL=application.js.map
|