@asenajs/ergenecore 1.0.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/LICENSE +21 -0
- package/README.md +636 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/Ergenecore.d.ts +598 -0
- package/dist/lib/Ergenecore.js +1406 -0
- package/dist/lib/Ergenecore.js.map +1 -0
- package/dist/lib/ErgenecoreContextWrapper.d.ts +190 -0
- package/dist/lib/ErgenecoreContextWrapper.js +387 -0
- package/dist/lib/ErgenecoreContextWrapper.js.map +1 -0
- package/dist/lib/ErgenecoreWebsocketAdapter.d.ts +68 -0
- package/dist/lib/ErgenecoreWebsocketAdapter.js +233 -0
- package/dist/lib/ErgenecoreWebsocketAdapter.js.map +1 -0
- package/dist/lib/defaults/ConfigService.d.ts +39 -0
- package/dist/lib/defaults/ConfigService.js +30 -0
- package/dist/lib/defaults/ConfigService.js.map +1 -0
- package/dist/lib/defaults/MiddlewareService.d.ts +39 -0
- package/dist/lib/defaults/MiddlewareService.js +30 -0
- package/dist/lib/defaults/MiddlewareService.js.map +1 -0
- package/dist/lib/defaults/StaticServeService.d.ts +24 -0
- package/dist/lib/defaults/StaticServeService.js +23 -0
- package/dist/lib/defaults/StaticServeService.js.map +1 -0
- package/dist/lib/defaults/ValidationService.d.ts +35 -0
- package/dist/lib/defaults/ValidationService.js +34 -0
- package/dist/lib/defaults/ValidationService.js.map +1 -0
- package/dist/lib/defaults/index.d.ts +25 -0
- package/dist/lib/defaults/index.js +26 -0
- package/dist/lib/defaults/index.js.map +1 -0
- package/dist/lib/errors.d.ts +84 -0
- package/dist/lib/errors.js +108 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/index.d.ts +36 -0
- package/dist/lib/index.js +37 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/middlewares/CorsMiddleware.d.ts +147 -0
- package/dist/lib/middlewares/CorsMiddleware.js +163 -0
- package/dist/lib/middlewares/CorsMiddleware.js.map +1 -0
- package/dist/lib/middlewares/RateLimiterMiddleware.d.ts +330 -0
- package/dist/lib/middlewares/RateLimiterMiddleware.js +300 -0
- package/dist/lib/middlewares/RateLimiterMiddleware.js.map +1 -0
- package/dist/lib/types/Handler.d.ts +65 -0
- package/dist/lib/types/Handler.js +10 -0
- package/dist/lib/types/Handler.js.map +1 -0
- package/dist/lib/types/StaticServeExtras.d.ts +29 -0
- package/dist/lib/types/StaticServeExtras.js +2 -0
- package/dist/lib/types/StaticServeExtras.js.map +1 -0
- package/dist/lib/types/Validation.d.ts +7 -0
- package/dist/lib/types/Validation.js +2 -0
- package/dist/lib/types/Validation.js.map +1 -0
- package/dist/lib/types/index.d.ts +24 -0
- package/dist/lib/types/index.js +25 -0
- package/dist/lib/types/index.js.map +1 -0
- package/dist/lib/utils/factory.d.ts +121 -0
- package/dist/lib/utils/factory.js +144 -0
- package/dist/lib/utils/factory.js.map +1 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 AsenaJs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
# Asena Ergenecore Adapter
|
|
2
|
+
|
|
3
|
+
**Blazing-fast** native Bun adapter for [Asena.js](https://github.com/asenajs/asena) - Built exclusively with Bun's native APIs for maximum performance.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ⚡ **Zero Dependencies** - Pure Bun native APIs (except Zod for validation)
|
|
8
|
+
- 🚀 **SIMD-Accelerated** - Native Bun router with SIMD-optimized matching
|
|
9
|
+
- 🔌 **Full Adapter Support** - HTTP, WebSocket, Middleware, Validation
|
|
10
|
+
- 📦 **Zero-Copy File Serving** - Bun.file() for optimal static file performance
|
|
11
|
+
- 🎯 **TypeScript First** - Complete type safety out of the box
|
|
12
|
+
- ✅ **90%+ Test Coverage** - Thoroughly tested with 212+ passing tests
|
|
13
|
+
- 🧩 **Controller-Based** - Seamless integration with Asena decorators
|
|
14
|
+
|
|
15
|
+
## Why Ergenecore?
|
|
16
|
+
|
|
17
|
+
Ergenecore is the **fastest** Asena adapter because it:
|
|
18
|
+
- Uses Bun's **native HTTP server** (`Bun.serve()`)
|
|
19
|
+
- Leverages **SIMD-accelerated routing** (no framework overhead)
|
|
20
|
+
- Implements **zero-copy** file serving with `Bun.file()`
|
|
21
|
+
- Has **no external dependencies** (except Zod)
|
|
22
|
+
- Is optimized for **Bun runtime** exclusively
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- [Bun](https://bun.sh) v1.3 or higher
|
|
27
|
+
- TypeScript v5.8.2 or higher
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bun add @asenajs/ergenecore
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
### Basic Server Setup
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { AsenaServerFactory } from '@asenajs/asena';
|
|
41
|
+
import { createErgenecoreAdapter } from '@asenajs/ergenecore/factory';
|
|
42
|
+
import { logger } from './logger';
|
|
43
|
+
|
|
44
|
+
// Create adapter with factory function
|
|
45
|
+
const adapter = createErgenecoreAdapter();
|
|
46
|
+
|
|
47
|
+
// Create and start server
|
|
48
|
+
const server = await AsenaServerFactory.create({
|
|
49
|
+
adapter,
|
|
50
|
+
logger,
|
|
51
|
+
port: 3000
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await server.start();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Controller Example
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { Controller } from '@asenajs/asena/server';
|
|
61
|
+
import { Get, Post, Put, Delete } from '@asenajs/asena/web';
|
|
62
|
+
import type { Context } from '@asenajs/ergenecore/types';
|
|
63
|
+
|
|
64
|
+
@Controller('/users')
|
|
65
|
+
export class UserController {
|
|
66
|
+
@Get({ path: '/' })
|
|
67
|
+
async list(context: Context) {
|
|
68
|
+
const page = context.getQuery('page') || '1';
|
|
69
|
+
return context.send({ users: [], page });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Get({ path: '/:id' })
|
|
73
|
+
async getById(context: Context) {
|
|
74
|
+
const id = context.getParam('id');
|
|
75
|
+
return context.send({ id, name: 'John Doe' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Post({ path: '/' })
|
|
79
|
+
async create(context: Context) {
|
|
80
|
+
const body = await context.getBody();
|
|
81
|
+
return context.send({ created: true, data: body }, 201);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Put({ path: '/:id' })
|
|
85
|
+
async update(context: Context) {
|
|
86
|
+
const id = context.getParam('id');
|
|
87
|
+
const body = await context.getBody();
|
|
88
|
+
return context.send({ id, updated: true, data: body });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Delete({ path: '/:id' })
|
|
92
|
+
async delete(context: Context) {
|
|
93
|
+
const id = context.getParam('id');
|
|
94
|
+
return context.send({ id, deleted: true }, 204);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## API Documentation
|
|
100
|
+
|
|
101
|
+
### Factory Functions
|
|
102
|
+
|
|
103
|
+
#### `createErgenecoreAdapter(options?)`
|
|
104
|
+
|
|
105
|
+
Creates a new Ergenecore adapter instance with optional configuration.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { AsenaServerFactory } from '@asenajs/asena';
|
|
109
|
+
import { createErgenecoreAdapter } from '@asenajs/ergenecore/factory';
|
|
110
|
+
import { logger } from './logger';
|
|
111
|
+
|
|
112
|
+
const adapter = createErgenecoreAdapter({
|
|
113
|
+
hostname: 'localhost',
|
|
114
|
+
enableWebSocket: true
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const server = await AsenaServerFactory.create({
|
|
118
|
+
adapter,
|
|
119
|
+
logger,
|
|
120
|
+
port: 3000
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await server.start();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Options:**
|
|
127
|
+
- `hostname` (string): Server hostname (default: undefined)
|
|
128
|
+
- `logger` (ServerLogger): Custom logger instance (optional, can be passed to AsenaServerFactory)
|
|
129
|
+
- `enableWebSocket` (boolean): Enable WebSocket support (default: true)
|
|
130
|
+
- `websocketAdapter` (ErgenecoreWebsocketAdapter): Custom WebSocket adapter
|
|
131
|
+
|
|
132
|
+
#### `createProductionAdapter(options?)`
|
|
133
|
+
|
|
134
|
+
Creates a production-optimized adapter with sensible defaults.
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { AsenaServerFactory } from '@asenajs/asena';
|
|
138
|
+
import { createProductionAdapter } from '@asenajs/ergenecore/factory';
|
|
139
|
+
import { logger } from './logger';
|
|
140
|
+
|
|
141
|
+
const adapter = createProductionAdapter({
|
|
142
|
+
hostname: '0.0.0.0'
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const server = await AsenaServerFactory.create({
|
|
146
|
+
adapter,
|
|
147
|
+
logger,
|
|
148
|
+
port: 8080
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await server.start();
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### `createDevelopmentAdapter(options?)`
|
|
155
|
+
|
|
156
|
+
Creates a development-friendly adapter with verbose logging.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { AsenaServerFactory } from '@asenajs/asena';
|
|
160
|
+
import { createDevelopmentAdapter } from '@asenajs/ergenecore/factory';
|
|
161
|
+
import { logger } from './logger';
|
|
162
|
+
|
|
163
|
+
const adapter = createDevelopmentAdapter();
|
|
164
|
+
|
|
165
|
+
const server = await AsenaServerFactory.create({
|
|
166
|
+
adapter,
|
|
167
|
+
logger,
|
|
168
|
+
port: 3000
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await server.start();
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Context API
|
|
175
|
+
|
|
176
|
+
The `Context` type provides access to request/response handling:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import type { Context } from '@asenajs/ergenecore/types';
|
|
180
|
+
|
|
181
|
+
// Get route parameters
|
|
182
|
+
const id = context.getParam('id');
|
|
183
|
+
|
|
184
|
+
// Get query parameters
|
|
185
|
+
const page = context.getQuery('page');
|
|
186
|
+
|
|
187
|
+
// Get request body
|
|
188
|
+
const body = await context.getBody();
|
|
189
|
+
|
|
190
|
+
// Get headers
|
|
191
|
+
const auth = context.getHeader('authorization');
|
|
192
|
+
|
|
193
|
+
// Send JSON response
|
|
194
|
+
return context.send({ success: true });
|
|
195
|
+
|
|
196
|
+
// Send with custom status
|
|
197
|
+
return context.send({ error: 'Not found' }, 404);
|
|
198
|
+
|
|
199
|
+
// Set cookies
|
|
200
|
+
context.setCookie('session', 'abc123', {
|
|
201
|
+
httpOnly: true,
|
|
202
|
+
secure: true,
|
|
203
|
+
maxAge: 86400
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Get cookies
|
|
207
|
+
const session = context.getCookie('session');
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Middleware
|
|
211
|
+
|
|
212
|
+
#### Custom Middleware
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { Middleware } from '@asenajs/asena/server';
|
|
216
|
+
import { Controller } from '@asenajs/asena/server';
|
|
217
|
+
import { Get } from '@asenajs/asena/web';
|
|
218
|
+
import { MiddlewareService, type Context } from '@asenajs/ergenecore';
|
|
219
|
+
|
|
220
|
+
@Middleware()
|
|
221
|
+
export class AuthMiddleware extends MiddlewareService {
|
|
222
|
+
async handle(context: Context, next: () => Promise<void>): Promise<any> {
|
|
223
|
+
const token = context.getHeader('authorization');
|
|
224
|
+
|
|
225
|
+
if (!token) {
|
|
226
|
+
return context.send({ error: 'Unauthorized' }, 401);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Verify token and set user in context
|
|
230
|
+
context.setValue('userId', 123);
|
|
231
|
+
|
|
232
|
+
await next();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Use in route
|
|
237
|
+
@Controller('/admin')
|
|
238
|
+
export class AdminController {
|
|
239
|
+
@Get({ path: '/dashboard', middlewares: [AuthMiddleware] })
|
|
240
|
+
async dashboard(context: Context) {
|
|
241
|
+
const userId = context.getValue('userId');
|
|
242
|
+
return context.send({ userId, message: 'Admin dashboard' });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### CORS Middleware
|
|
248
|
+
|
|
249
|
+
Ergenecore comes with a built-in CORS middleware for handling Cross-Origin Resource Sharing:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { Middleware } from '@asenajs/asena/server';
|
|
253
|
+
import { CorsMiddleware } from '@asenajs/ergenecore';
|
|
254
|
+
|
|
255
|
+
// Allow all origins (default)
|
|
256
|
+
@Middleware()
|
|
257
|
+
export class GlobalCors extends CorsMiddleware {
|
|
258
|
+
constructor() {
|
|
259
|
+
super(); // Defaults to { origin: '*' }
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Whitelist specific origins
|
|
264
|
+
@Middleware()
|
|
265
|
+
export class RestrictedCors extends CorsMiddleware {
|
|
266
|
+
constructor() {
|
|
267
|
+
super({
|
|
268
|
+
origin: ['https://example.com', 'https://app.example.com'],
|
|
269
|
+
credentials: true,
|
|
270
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
271
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
272
|
+
exposedHeaders: ['X-Total-Count'],
|
|
273
|
+
maxAge: 86400 // 24 hours
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Dynamic origin validation
|
|
279
|
+
@Middleware()
|
|
280
|
+
export class DynamicCors extends CorsMiddleware {
|
|
281
|
+
constructor() {
|
|
282
|
+
super({
|
|
283
|
+
origin: (origin: string) => {
|
|
284
|
+
// Allow all subdomains of example.com
|
|
285
|
+
return origin.endsWith('.example.com') || origin === 'https://example.com';
|
|
286
|
+
},
|
|
287
|
+
credentials: true
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Use globally in server config
|
|
293
|
+
import { Config } from '@asenajs/asena/server';
|
|
294
|
+
import { ConfigService } from '@asenajs/ergenecore';
|
|
295
|
+
|
|
296
|
+
@Config()
|
|
297
|
+
export class ServerConfig extends ConfigService {
|
|
298
|
+
middlewares = [GlobalCors];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Or use per-route
|
|
302
|
+
@Controller('/api')
|
|
303
|
+
export class ApiController {
|
|
304
|
+
@Get({ path: '/public', middlewares: [RestrictedCors] })
|
|
305
|
+
async publicData(context: Context) {
|
|
306
|
+
return context.send({ data: 'public' });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### Rate Limiter Middleware
|
|
312
|
+
|
|
313
|
+
Ergenecore includes a Token Bucket-based rate limiter for controlling request rates and preventing abuse:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { Middleware } from '@asenajs/asena/server';
|
|
317
|
+
import { RateLimiterMiddleware } from '@asenajs/ergenecore';
|
|
318
|
+
|
|
319
|
+
// General API rate limiter (100 requests per minute)
|
|
320
|
+
@Middleware()
|
|
321
|
+
export class ApiRateLimiter extends RateLimiterMiddleware {
|
|
322
|
+
constructor() {
|
|
323
|
+
super({
|
|
324
|
+
capacity: 100,
|
|
325
|
+
refillRate: 100 / 60, // 100 requests per minute
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Strict rate limiter for sensitive endpoints (5 requests per minute)
|
|
331
|
+
@Middleware()
|
|
332
|
+
export class StrictRateLimiter extends RateLimiterMiddleware {
|
|
333
|
+
constructor() {
|
|
334
|
+
super({
|
|
335
|
+
capacity: 5,
|
|
336
|
+
refillRate: 5 / 60, // 5 requests per minute
|
|
337
|
+
message: 'Too many login attempts. Please try again later.',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Custom rate limiter with advanced options
|
|
343
|
+
@Middleware()
|
|
344
|
+
export class CustomRateLimiter extends RateLimiterMiddleware {
|
|
345
|
+
constructor() {
|
|
346
|
+
super({
|
|
347
|
+
capacity: 50,
|
|
348
|
+
refillRate: 50 / 60, // 50 requests per minute
|
|
349
|
+
|
|
350
|
+
// Rate limit by user ID instead of IP
|
|
351
|
+
keyGenerator: (ctx) => ctx.state.user?.id || 'anonymous',
|
|
352
|
+
|
|
353
|
+
// Skip rate limiting for admin users
|
|
354
|
+
skip: (ctx) => ctx.state.user?.role === 'admin',
|
|
355
|
+
|
|
356
|
+
// Expensive operations cost more tokens
|
|
357
|
+
cost: (ctx) => ctx.req.url.includes('/search') ? 5 : 1,
|
|
358
|
+
|
|
359
|
+
// Custom response message
|
|
360
|
+
message: 'Rate limit exceeded. Please slow down.',
|
|
361
|
+
statusCode: 429,
|
|
362
|
+
|
|
363
|
+
// Cleanup inactive buckets every 60 seconds
|
|
364
|
+
cleanupInterval: 60000,
|
|
365
|
+
bucketTTL: 600000 // 10 minutes
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Use globally
|
|
371
|
+
import { Config } from '@asenajs/asena/server';
|
|
372
|
+
import { ConfigService } from '@asenajs/ergenecore';
|
|
373
|
+
|
|
374
|
+
@Config()
|
|
375
|
+
export class ServerConfig extends ConfigService {
|
|
376
|
+
middlewares = [ApiRateLimiter];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Use per-controller
|
|
380
|
+
@Controller('/api', { middlewares: [ApiRateLimiter] })
|
|
381
|
+
export class ApiController {
|
|
382
|
+
@Get({ path: '/users' })
|
|
383
|
+
async getUsers(context: Context) {
|
|
384
|
+
return context.send({ users: [] });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Use per-route for stricter limits
|
|
389
|
+
@Controller('/auth')
|
|
390
|
+
export class AuthController {
|
|
391
|
+
@Post({ path: '/login', middlewares: [StrictRateLimiter] })
|
|
392
|
+
async login(context: Context) {
|
|
393
|
+
const body = await context.getBody();
|
|
394
|
+
// Login logic
|
|
395
|
+
return context.send({ token: 'abc123' });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Rate Limiter Options:**
|
|
401
|
+
- `capacity` (number): Maximum token bucket size (burst capacity). Default: `100`
|
|
402
|
+
- `refillRate` (number): Tokens refilled per second. Default: `10`
|
|
403
|
+
- `keyGenerator` (function): Custom function to identify clients. Default: IP-based
|
|
404
|
+
- `message` (string): Custom error message. Default: `'Rate limit exceeded. Please try again later.'`
|
|
405
|
+
- `statusCode` (number): HTTP status code for rate limit response. Default: `429`
|
|
406
|
+
- `cost` (number | function): Token cost per request. Default: `1`
|
|
407
|
+
- `skip` (function): Skip rate limiting based on context. Default: `undefined`
|
|
408
|
+
- `cleanupInterval` (number): Cleanup interval in milliseconds. Default: `60000` (1 minute)
|
|
409
|
+
- `bucketTTL` (number): Inactive bucket TTL in milliseconds. Default: `600000` (10 minutes)
|
|
410
|
+
|
|
411
|
+
**Rate Limit Headers:**
|
|
412
|
+
|
|
413
|
+
The middleware automatically sets standard rate limit headers:
|
|
414
|
+
- `X-RateLimit-Limit`: Requests allowed per minute
|
|
415
|
+
- `X-RateLimit-Remaining`: Remaining tokens in bucket
|
|
416
|
+
- `X-RateLimit-Reset`: Unix timestamp when bucket resets
|
|
417
|
+
- `Retry-After`: Seconds to wait before retrying (on 429 response)
|
|
418
|
+
|
|
419
|
+
### Validation with Zod
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
import { Controller, Middleware } from '@asenajs/asena/server';
|
|
423
|
+
import { Post } from '@asenajs/asena/web';
|
|
424
|
+
import { ValidationService, type Context, type ValidationSchema, type ValidationSchemaWithHook } from '@asenajs/ergenecore';
|
|
425
|
+
import { z } from 'zod';
|
|
426
|
+
|
|
427
|
+
@Middleware({ validator: true })
|
|
428
|
+
export class CreateUserValidator extends ValidationService {
|
|
429
|
+
json(): ValidationSchema | ValidationSchemaWithHook {
|
|
430
|
+
return z.object({
|
|
431
|
+
name: z.string({ message: 'Name is required' }).min(3).max(50),
|
|
432
|
+
email: z.string({ message: 'Email is required' }).email(),
|
|
433
|
+
age: z.number({ message: 'Age must be a number' }).min(18)
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Use in controller
|
|
439
|
+
@Controller('/users')
|
|
440
|
+
export class UserController {
|
|
441
|
+
@Post({ path: '/', validator: CreateUserValidator })
|
|
442
|
+
async create(context: Context) {
|
|
443
|
+
const body = await context.getBody();
|
|
444
|
+
|
|
445
|
+
// Body is automatically validated by Asena
|
|
446
|
+
return context.send({ created: true, data: body }, 201);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Note:** The `json()` method can also return a Promise for async validation schemas:
|
|
452
|
+
```typescript
|
|
453
|
+
async json(): Promise<ValidationSchema | ValidationSchemaWithHook> {
|
|
454
|
+
return z.object({ /* schema */ });
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Combining Middleware and Validation
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
import { Controller, Middleware } from '@asenajs/asena/server';
|
|
462
|
+
import { Post } from '@asenajs/asena/web';
|
|
463
|
+
import {
|
|
464
|
+
MiddlewareService,
|
|
465
|
+
ValidationService,
|
|
466
|
+
type Context,
|
|
467
|
+
type ValidationSchema,
|
|
468
|
+
type ValidationSchemaWithHook
|
|
469
|
+
} from '@asenajs/ergenecore';
|
|
470
|
+
import { z } from 'zod';
|
|
471
|
+
|
|
472
|
+
// Authentication middleware
|
|
473
|
+
@Middleware()
|
|
474
|
+
export class AuthMiddleware extends MiddlewareService {
|
|
475
|
+
async handle(context: Context, next: () => Promise<void>): Promise<any> {
|
|
476
|
+
const token = context.getHeader('authorization');
|
|
477
|
+
|
|
478
|
+
if (!token) {
|
|
479
|
+
return context.send({ error: 'Unauthorized' }, 401);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Verify token and set user
|
|
483
|
+
context.setValue('userId', 123);
|
|
484
|
+
await next();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Validation
|
|
489
|
+
@Middleware({ validator: true })
|
|
490
|
+
export class CreatePostValidator extends ValidationService {
|
|
491
|
+
json(): ValidationSchema | ValidationSchemaWithHook {
|
|
492
|
+
return z.object({
|
|
493
|
+
title: z.string({ message: 'Title is required' }).min(5),
|
|
494
|
+
content: z.string({ message: 'Content is required' }).min(10)
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Controller with both middleware and validation
|
|
500
|
+
@Controller('/posts')
|
|
501
|
+
export class PostController {
|
|
502
|
+
@Post({ path: '/', validator: CreatePostValidator, middlewares: [AuthMiddleware] })
|
|
503
|
+
async create(context: Context) {
|
|
504
|
+
const body = await context.getBody();
|
|
505
|
+
const userId = context.getValue('userId');
|
|
506
|
+
|
|
507
|
+
return context.send({
|
|
508
|
+
created: true,
|
|
509
|
+
userId,
|
|
510
|
+
post: body
|
|
511
|
+
}, 201);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### WebSocket Support
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
import { WebSocket } from '@asenajs/asena/web-socket';
|
|
520
|
+
import { AsenaWebSocketService } from '@asenajs/asena/web-socket';
|
|
521
|
+
import type { Socket } from '@asenajs/asena/web-socket';
|
|
522
|
+
|
|
523
|
+
@WebSocket({ path: '/chat', name: 'ChatSocket' })
|
|
524
|
+
export class ChatSocket extends AsenaWebSocketService<void> {
|
|
525
|
+
protected async onOpen(ws: Socket<void>): Promise<void> {
|
|
526
|
+
console.log('Client connected:', ws.id);
|
|
527
|
+
ws.send('Welcome to chat!');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
protected async onMessage(ws: Socket<void>, message: string): Promise<void> {
|
|
531
|
+
console.log('Received:', message);
|
|
532
|
+
|
|
533
|
+
// Echo message back to client
|
|
534
|
+
ws.send(`Echo: ${message}`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
protected async onClose(ws: Socket<void>): Promise<void> {
|
|
538
|
+
console.log('Client disconnected:', ws.id);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Static File Serving
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
import { Controller } from '@asenajs/asena/server';
|
|
547
|
+
import { Get } from '@asenajs/asena/web';
|
|
548
|
+
import { StaticServe, StaticServeService } from '@asenajs/asena/static';
|
|
549
|
+
import type { Context } from '@asenajs/ergenecore/types';
|
|
550
|
+
|
|
551
|
+
@StaticServe({ root: './public' })
|
|
552
|
+
export class StaticMiddleware extends StaticServeService {
|
|
553
|
+
rewriteRequestPath(path: string): string {
|
|
554
|
+
return path.replace(/^\/static\/|^static\//, '');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
onFound(_path: string, _context: Context): void | Promise<void> {
|
|
558
|
+
console.log('File found and served');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
onNotFound(path: string, context: Context): void | Promise<void> {
|
|
562
|
+
console.log(`${path} not found, requested: ${context.getRequest().url}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Use in controller
|
|
567
|
+
@Controller('/static')
|
|
568
|
+
export class StaticController {
|
|
569
|
+
@Get({ path: '/*', staticServe: StaticMiddleware })
|
|
570
|
+
static() {}
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Error Handling
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
import { Config } from '@asenajs/asena/server';
|
|
578
|
+
import { Inject } from '@asenajs/asena/ioc';
|
|
579
|
+
import { ConfigService, type Context } from '@asenajs/ergenecore';
|
|
580
|
+
|
|
581
|
+
@Config()
|
|
582
|
+
export class ServerConfig extends ConfigService {
|
|
583
|
+
onError(error: Error, context: Context): Response | Promise<Response> {
|
|
584
|
+
console.error('Error:', error);
|
|
585
|
+
|
|
586
|
+
return context.send({
|
|
587
|
+
error: error.message,
|
|
588
|
+
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
|
589
|
+
}, 500);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// With custom exception mapper
|
|
594
|
+
export class ExceptionMapper {
|
|
595
|
+
map(error: Error, context: Context): Response {
|
|
596
|
+
// Custom error mapping logic
|
|
597
|
+
if (error.name === 'ValidationError') {
|
|
598
|
+
return context.send({ error: 'Validation failed', details: error.message }, 400);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return context.send({ error: 'Internal server error' }, 500);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
@Config()
|
|
606
|
+
export class ServerConfigWithMapper extends ConfigService {
|
|
607
|
+
@Inject('ExceptionMapper')
|
|
608
|
+
private mapper: ExceptionMapper;
|
|
609
|
+
|
|
610
|
+
onError(error: Error, context: Context): Response | Promise<Response> {
|
|
611
|
+
return this.mapper.map(error, context);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
## Contributing
|
|
617
|
+
|
|
618
|
+
Contributions are welcome! Please follow these guidelines:
|
|
619
|
+
|
|
620
|
+
1. Follow the **TDD approach** (write tests first)
|
|
621
|
+
2. Maintain **90%+ test coverage**
|
|
622
|
+
3. Use **Bun native APIs** only (no external dependencies except Zod)
|
|
623
|
+
4. Follow the **CLAUDE.md** guidelines
|
|
624
|
+
|
|
625
|
+
## License
|
|
626
|
+
|
|
627
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
628
|
+
|
|
629
|
+
## Related Projects
|
|
630
|
+
|
|
631
|
+
- [Asena.js](https://asena.sh) - The core framework
|
|
632
|
+
- [Bun](https://bun.sh) - The JavaScript runtime
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
**Built with ⚡ by the Asena.js team**
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asena Ergenecore - Native Bun Adapter for Asenajs
|
|
3
|
+
*
|
|
4
|
+
* This is the main entry point for the package.
|
|
5
|
+
* All public APIs are exported from lib/index.ts
|
|
6
|
+
*
|
|
7
|
+
* @module @asenajs/ergenecore
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { AsenaServerFactory } from '@asenajs/asena';
|
|
12
|
+
* import { createErgenecoreAdapter, type Context } from '@asenajs/ergenecore';
|
|
13
|
+
*
|
|
14
|
+
* const adapter = createErgenecoreAdapter();
|
|
15
|
+
* const server = await AsenaServerFactory.create({
|
|
16
|
+
* adapter,
|
|
17
|
+
* logger,
|
|
18
|
+
* port: 3000
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* await server.start();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export * from './lib/index';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asena Ergenecore - Native Bun Adapter for Asenajs
|
|
3
|
+
*
|
|
4
|
+
* This is the main entry point for the package.
|
|
5
|
+
* All public APIs are exported from lib/index.ts
|
|
6
|
+
*
|
|
7
|
+
* @module @asenajs/ergenecore
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { AsenaServerFactory } from '@asenajs/asena';
|
|
12
|
+
* import { createErgenecoreAdapter, type Context } from '@asenajs/ergenecore';
|
|
13
|
+
*
|
|
14
|
+
* const adapter = createErgenecoreAdapter();
|
|
15
|
+
* const server = await AsenaServerFactory.create({
|
|
16
|
+
* adapter,
|
|
17
|
+
* logger,
|
|
18
|
+
* port: 3000
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* await server.start();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export * from './lib/index';
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,cAAc,aAAa,CAAC"}
|