@bunary/http 0.0.2 → 0.0.4
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/README.md +166 -3
- package/dist/app.d.ts +7 -25
- package/dist/app.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +342 -41
- package/dist/pathUtils.d.ts +34 -0
- package/dist/pathUtils.d.ts.map +1 -0
- package/dist/response.d.ts +26 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/router.d.ts +49 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/routes/builder.d.ts +17 -0
- package/dist/routes/builder.d.ts.map +1 -0
- package/dist/routes/find.d.ts +27 -0
- package/dist/routes/find.d.ts.map +1 -0
- package/dist/routes/group.d.ts +7 -0
- package/dist/routes/group.d.ts.map +1 -0
- package/dist/routes/index.d.ts +4 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/types/appOptions.d.ts +8 -0
- package/dist/types/appOptions.d.ts.map +1 -0
- package/dist/types/bunaryApp.d.ts +97 -0
- package/dist/types/bunaryApp.d.ts.map +1 -0
- package/dist/types/bunaryServer.d.ts +14 -0
- package/dist/types/bunaryServer.d.ts.map +1 -0
- package/dist/types/groupOptions.d.ts +13 -0
- package/dist/types/groupOptions.d.ts.map +1 -0
- package/dist/types/groupRouter.d.ts +26 -0
- package/dist/types/groupRouter.d.ts.map +1 -0
- package/dist/types/handlerResponse.d.ts +8 -0
- package/dist/types/handlerResponse.d.ts.map +1 -0
- package/dist/types/httpMethod.d.ts +5 -0
- package/dist/types/httpMethod.d.ts.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/middleware.d.ts +21 -0
- package/dist/types/middleware.d.ts.map +1 -0
- package/dist/types/pathParams.d.ts +6 -0
- package/dist/types/pathParams.d.ts.map +1 -0
- package/dist/types/queryParams.d.ts +5 -0
- package/dist/types/queryParams.d.ts.map +1 -0
- package/dist/types/requestContext.d.ts +22 -0
- package/dist/types/requestContext.d.ts.map +1 -0
- package/dist/types/route.d.ts +27 -0
- package/dist/types/route.d.ts.map +1 -0
- package/dist/types/routeBuilder.d.ts +24 -0
- package/dist/types/routeBuilder.d.ts.map +1 -0
- package/dist/types/routeHandler.d.ts +17 -0
- package/dist/types/routeHandler.d.ts.map +1 -0
- package/dist/types/routeInfo.d.ts +13 -0
- package/dist/types/routeInfo.d.ts.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,10 @@ Part of the [Bunary](https://github.com/bunary-dev) ecosystem - a Bun-first back
|
|
|
11
11
|
- 🔒 **Type-safe** - Full TypeScript support with strict types
|
|
12
12
|
- ⚡ **Fast** - Minimal overhead, direct routing
|
|
13
13
|
- 🧩 **Simple API** - Chainable route registration with automatic JSON serialization
|
|
14
|
+
- 📂 **Route Groups** - Organize routes with shared prefixes, middleware, and name prefixes
|
|
15
|
+
- 🏷️ **Named Routes** - URL generation with route names
|
|
16
|
+
- ✅ **Route Constraints** - Validate parameters with regex patterns
|
|
17
|
+
- ❓ **Optional Parameters** - Flexible routes with optional path segments
|
|
14
18
|
|
|
15
19
|
## Installation
|
|
16
20
|
|
|
@@ -215,9 +219,163 @@ app.use(async (ctx, next) => {
|
|
|
215
219
|
});
|
|
216
220
|
```
|
|
217
221
|
|
|
222
|
+
## Route Groups
|
|
223
|
+
|
|
224
|
+
Group routes together with shared prefixes, middleware, and name prefixes.
|
|
225
|
+
|
|
226
|
+
### Basic Groups
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// Simple prefix
|
|
230
|
+
app.group('/api', (router) => {
|
|
231
|
+
router.get('/users', () => ({ users: [] })); // /api/users
|
|
232
|
+
router.get('/posts', () => ({ posts: [] })); // /api/posts
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Groups with Options
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// Auth middleware for protected routes
|
|
240
|
+
const authMiddleware = async (ctx, next) => {
|
|
241
|
+
const token = ctx.request.headers.get('Authorization');
|
|
242
|
+
if (!token) return new Response('Unauthorized', { status: 401 });
|
|
243
|
+
return await next();
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
app.group({
|
|
247
|
+
prefix: '/admin',
|
|
248
|
+
middleware: [authMiddleware],
|
|
249
|
+
name: 'admin.'
|
|
250
|
+
}, (router) => {
|
|
251
|
+
router.get('/dashboard', () => ({})).name('dashboard'); // name: admin.dashboard
|
|
252
|
+
router.get('/users', () => ({})).name('users'); // name: admin.users
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Nested Groups
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
app.group('/api', (api) => {
|
|
260
|
+
api.group('/v1', (v1) => {
|
|
261
|
+
v1.get('/users', () => ({})); // /api/v1/users
|
|
262
|
+
});
|
|
263
|
+
api.group('/v2', (v2) => {
|
|
264
|
+
v2.get('/users', () => ({})); // /api/v2/users
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Named Routes
|
|
270
|
+
|
|
271
|
+
Assign names to routes for URL generation.
|
|
272
|
+
|
|
273
|
+
### Naming Routes
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
app.get('/users/:id', (ctx) => ({})).name('users.show');
|
|
277
|
+
app.get('/posts/:slug', (ctx) => ({})).name('posts.show');
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Generating URLs
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// Basic URL generation
|
|
284
|
+
const url = app.route('users.show', { id: 42 });
|
|
285
|
+
// "/users/42"
|
|
286
|
+
|
|
287
|
+
// With query string
|
|
288
|
+
const searchUrl = app.route('users.show', { id: 42, tab: 'profile' });
|
|
289
|
+
// "/users/42?tab=profile"
|
|
290
|
+
|
|
291
|
+
// Check if route exists
|
|
292
|
+
if (app.hasRoute('users.show')) {
|
|
293
|
+
// ...
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// List all routes
|
|
297
|
+
const routes = app.getRoutes();
|
|
298
|
+
// [{ name: 'users.show', method: 'GET', path: '/users/:id' }, ...]
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Route Constraints
|
|
302
|
+
|
|
303
|
+
Add regex constraints to validate route parameters.
|
|
304
|
+
|
|
305
|
+
### Basic Constraints
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// Only match if :id is numeric
|
|
309
|
+
app.get('/users/:id', (ctx) => ({}))
|
|
310
|
+
.where('id', /^\d+$/);
|
|
311
|
+
|
|
312
|
+
// Using string pattern
|
|
313
|
+
app.get('/posts/:slug', (ctx) => ({}))
|
|
314
|
+
.where('slug', '^[a-z0-9-]+$');
|
|
315
|
+
|
|
316
|
+
// Multiple constraints
|
|
317
|
+
app.get('/users/:id/posts/:postId', (ctx) => ({}))
|
|
318
|
+
.where({ id: /^\d+$/, postId: /^\d+$/ });
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Helper Methods
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// whereNumber - digits only
|
|
325
|
+
app.get('/users/:id', () => ({})).whereNumber('id');
|
|
326
|
+
|
|
327
|
+
// whereAlpha - letters only (a-zA-Z)
|
|
328
|
+
app.get('/categories/:name', () => ({})).whereAlpha('name');
|
|
329
|
+
|
|
330
|
+
// whereAlphaNumeric - letters and digits
|
|
331
|
+
app.get('/codes/:code', () => ({})).whereAlphaNumeric('code');
|
|
332
|
+
|
|
333
|
+
// whereUuid - UUID format
|
|
334
|
+
app.get('/items/:uuid', () => ({})).whereUuid('uuid');
|
|
335
|
+
|
|
336
|
+
// whereUlid - ULID format
|
|
337
|
+
app.get('/records/:ulid', () => ({})).whereUlid('ulid');
|
|
338
|
+
|
|
339
|
+
// whereIn - specific allowed values
|
|
340
|
+
app.get('/status/:status', () => ({})).whereIn('status', ['active', 'pending', 'archived']);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Chaining Constraints
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
app.get('/users/:id/posts/:slug', (ctx) => ({}))
|
|
347
|
+
.whereNumber('id')
|
|
348
|
+
.whereAlpha('slug')
|
|
349
|
+
.name('users.posts');
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Optional Parameters
|
|
353
|
+
|
|
354
|
+
Use `?` to mark route parameters as optional.
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// :id is optional
|
|
358
|
+
app.get('/users/:id?', (ctx) => {
|
|
359
|
+
if (ctx.params.id) {
|
|
360
|
+
return { user: ctx.params.id };
|
|
361
|
+
}
|
|
362
|
+
return { users: [] };
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Multiple optional params
|
|
366
|
+
app.get('/archive/:year?/:month?', (ctx) => {
|
|
367
|
+
const { year, month } = ctx.params;
|
|
368
|
+
// year and month may be undefined
|
|
369
|
+
return { year, month };
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Constraints work with optional params
|
|
373
|
+
app.get('/posts/:id?', (ctx) => ({})).whereNumber('id');
|
|
374
|
+
```
|
|
375
|
+
|
|
218
376
|
## Error Handling
|
|
219
377
|
|
|
220
|
-
Uncaught errors in handlers return a 500 response:
|
|
378
|
+
Uncaught errors in handlers return a 500 response with the error message:
|
|
221
379
|
|
|
222
380
|
```typescript
|
|
223
381
|
app.get('/error', () => {
|
|
@@ -225,7 +383,7 @@ app.get('/error', () => {
|
|
|
225
383
|
});
|
|
226
384
|
|
|
227
385
|
// Returns: 500 Internal Server Error
|
|
228
|
-
// Body: { error: "
|
|
386
|
+
// Body: { error: "Something went wrong" }
|
|
229
387
|
```
|
|
230
388
|
|
|
231
389
|
## Types
|
|
@@ -238,7 +396,12 @@ import type {
|
|
|
238
396
|
BunaryServer,
|
|
239
397
|
RequestContext,
|
|
240
398
|
RouteHandler,
|
|
241
|
-
Middleware
|
|
399
|
+
Middleware,
|
|
400
|
+
RouteBuilder,
|
|
401
|
+
GroupOptions,
|
|
402
|
+
GroupRouter,
|
|
403
|
+
GroupCallback,
|
|
404
|
+
RouteInfo
|
|
242
405
|
} from '@bunary/http';
|
|
243
406
|
```
|
|
244
407
|
|
package/dist/app.d.ts
CHANGED
|
@@ -1,21 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Create a new Bunary HTTP application instance.
|
|
3
|
-
*
|
|
4
|
-
* @returns BunaryApp instance with routing and middleware support
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* ```ts
|
|
8
|
-
* import { createApp } from "@bunary/http";
|
|
9
|
-
*
|
|
10
|
-
* const app = createApp();
|
|
11
|
-
*
|
|
12
|
-
* app.get("/", () => ({ message: "Hello!" }));
|
|
13
|
-
* app.get("/users/:id", (ctx) => ({ id: ctx.params.id }));
|
|
14
|
-
*
|
|
15
|
-
* app.listen(3000);
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
import type { BunaryApp } from "./types.js";
|
|
1
|
+
import type { BunaryApp } from "./types/index.js";
|
|
19
2
|
/**
|
|
20
3
|
* Create a new Bunary HTTP application instance.
|
|
21
4
|
*
|
|
@@ -38,15 +21,14 @@ import type { BunaryApp } from "./types.js";
|
|
|
38
21
|
* return { id: ctx.params.id };
|
|
39
22
|
* });
|
|
40
23
|
*
|
|
41
|
-
* //
|
|
42
|
-
* app.
|
|
43
|
-
*
|
|
24
|
+
* // Route groups
|
|
25
|
+
* app.group("/api", (router) => {
|
|
26
|
+
* router.get("/users", () => ({ users: [] }));
|
|
44
27
|
* });
|
|
45
28
|
*
|
|
46
|
-
* //
|
|
47
|
-
* app.get("/
|
|
48
|
-
*
|
|
49
|
-
* });
|
|
29
|
+
* // Named routes
|
|
30
|
+
* app.get("/users/:id", (ctx) => ({ id: ctx.params.id })).name("users.show");
|
|
31
|
+
* const url = app.route("users.show", { id: 123 });
|
|
50
32
|
*
|
|
51
33
|
* app.listen(3000);
|
|
52
34
|
* ```
|
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACX,SAAS,EAYT,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,SAAS,IAAI,SAAS,CAmOrC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,6 @@
|
|
|
18
18
|
*
|
|
19
19
|
* @packageDocumentation
|
|
20
20
|
*/
|
|
21
|
-
export type { AppOptions, BunaryApp, BunaryServer, HandlerResponse, HttpMethod, Middleware, PathParams, QueryParams, RequestContext, Route, RouteHandler, } from "./types.js";
|
|
21
|
+
export type { AppOptions, BunaryApp, BunaryServer, GroupCallback, GroupOptions, GroupRouter, HandlerResponse, HttpMethod, Middleware, PathParams, QueryParams, RequestContext, Route, RouteBuilder, RouteHandler, RouteInfo, } from "./types/index.js";
|
|
22
22
|
export { createApp } from "./app.js";
|
|
23
23
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,YAAY,EACX,UAAU,EACV,SAAS,EACT,YAAY,EACZ,eAAe,EACf,UAAU,EACV,UAAU,EACV,UAAU,EACV,WAAW,EACX,cAAc,EACd,KAAK,EACL,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,YAAY,EACX,UAAU,EACV,SAAS,EACT,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,EACX,eAAe,EACf,UAAU,EACV,UAAU,EACV,UAAU,EACV,WAAW,EACX,cAAc,EACd,KAAK,EACL,YAAY,EACZ,YAAY,EACZ,SAAS,GACT,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,26 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/
|
|
3
|
-
function compilePath(path) {
|
|
4
|
-
const paramNames = [];
|
|
5
|
-
const regexString = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_match, paramName) => {
|
|
6
|
-
paramNames.push(paramName);
|
|
7
|
-
return "([^/]+)";
|
|
8
|
-
});
|
|
9
|
-
return {
|
|
10
|
-
pattern: new RegExp(`^${regexString}$`),
|
|
11
|
-
paramNames
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
function extractParams(path, route) {
|
|
15
|
-
const match = path.match(route.pattern);
|
|
16
|
-
if (!match)
|
|
17
|
-
return {};
|
|
18
|
-
const params = {};
|
|
19
|
-
for (let i = 0;i < route.paramNames.length; i++) {
|
|
20
|
-
params[route.paramNames[i]] = match[i + 1];
|
|
21
|
-
}
|
|
22
|
-
return params;
|
|
23
|
-
}
|
|
2
|
+
// src/response.ts
|
|
24
3
|
function toResponse(result) {
|
|
25
4
|
if (result instanceof Response) {
|
|
26
5
|
return result;
|
|
@@ -45,34 +24,296 @@ function toResponse(result) {
|
|
|
45
24
|
headers: { "Content-Type": "application/json" }
|
|
46
25
|
});
|
|
47
26
|
}
|
|
27
|
+
|
|
28
|
+
// src/router.ts
|
|
29
|
+
function compilePath(path) {
|
|
30
|
+
const paramNames = [];
|
|
31
|
+
const optionalParams = [];
|
|
32
|
+
let regexString = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
|
+
regexString = regexString.replace(/\/:([a-zA-Z_][a-zA-Z0-9_]*)(\\\?)?/g, (_match, paramName, isOptional) => {
|
|
34
|
+
if (paramNames.includes(paramName)) {
|
|
35
|
+
throw new Error(`Duplicate parameter name ":${paramName}" in route pattern "${path}". Each parameter name must be unique within a route.`);
|
|
36
|
+
}
|
|
37
|
+
paramNames.push(paramName);
|
|
38
|
+
if (isOptional) {
|
|
39
|
+
optionalParams.push(paramName);
|
|
40
|
+
return "(?:/([^/]+))?";
|
|
41
|
+
}
|
|
42
|
+
return "/([^/]+)";
|
|
43
|
+
});
|
|
44
|
+
regexString += "/?";
|
|
45
|
+
return {
|
|
46
|
+
pattern: new RegExp(`^${regexString}$`),
|
|
47
|
+
paramNames,
|
|
48
|
+
optionalParams
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function extractParams(path, route) {
|
|
52
|
+
const match = path.match(route.pattern);
|
|
53
|
+
if (!match)
|
|
54
|
+
return {};
|
|
55
|
+
const params = {};
|
|
56
|
+
for (let i = 0;i < route.paramNames.length; i++) {
|
|
57
|
+
const value = match[i + 1];
|
|
58
|
+
if (value !== undefined && value !== "") {
|
|
59
|
+
params[route.paramNames[i]] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return params;
|
|
63
|
+
}
|
|
64
|
+
function checkConstraints(params, constraints) {
|
|
65
|
+
if (!constraints)
|
|
66
|
+
return true;
|
|
67
|
+
for (const [param, pattern] of Object.entries(constraints)) {
|
|
68
|
+
const value = params[param];
|
|
69
|
+
if (value === undefined)
|
|
70
|
+
continue;
|
|
71
|
+
if (!pattern.test(value))
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/routes/builder.ts
|
|
78
|
+
function compilePattern(pattern, param) {
|
|
79
|
+
try {
|
|
80
|
+
return new RegExp(pattern);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : "Invalid pattern";
|
|
83
|
+
throw new Error(`Invalid regex pattern for parameter "${param}": ${message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function createRouteBuilder(route, namedRoutes, app) {
|
|
87
|
+
function addConstraint(param, pattern) {
|
|
88
|
+
if (!route.constraints) {
|
|
89
|
+
route.constraints = {};
|
|
90
|
+
}
|
|
91
|
+
route.constraints[param] = pattern;
|
|
92
|
+
}
|
|
93
|
+
const builder = {
|
|
94
|
+
get get() {
|
|
95
|
+
return app.get;
|
|
96
|
+
},
|
|
97
|
+
get post() {
|
|
98
|
+
return app.post;
|
|
99
|
+
},
|
|
100
|
+
get put() {
|
|
101
|
+
return app.put;
|
|
102
|
+
},
|
|
103
|
+
get delete() {
|
|
104
|
+
return app.delete;
|
|
105
|
+
},
|
|
106
|
+
get patch() {
|
|
107
|
+
return app.patch;
|
|
108
|
+
},
|
|
109
|
+
get use() {
|
|
110
|
+
return app.use;
|
|
111
|
+
},
|
|
112
|
+
get group() {
|
|
113
|
+
return app.group;
|
|
114
|
+
},
|
|
115
|
+
get route() {
|
|
116
|
+
return app.route;
|
|
117
|
+
},
|
|
118
|
+
get hasRoute() {
|
|
119
|
+
return app.hasRoute;
|
|
120
|
+
},
|
|
121
|
+
get getRoutes() {
|
|
122
|
+
return app.getRoutes;
|
|
123
|
+
},
|
|
124
|
+
get listen() {
|
|
125
|
+
return app.listen;
|
|
126
|
+
},
|
|
127
|
+
get fetch() {
|
|
128
|
+
return app.fetch;
|
|
129
|
+
},
|
|
130
|
+
name: (name) => {
|
|
131
|
+
if (namedRoutes.has(name)) {
|
|
132
|
+
throw new Error(`Route name "${name}" is already defined`);
|
|
133
|
+
}
|
|
134
|
+
route.name = name;
|
|
135
|
+
namedRoutes.set(name, route);
|
|
136
|
+
return builder;
|
|
137
|
+
},
|
|
138
|
+
where: (paramOrConstraints, pattern) => {
|
|
139
|
+
if (typeof paramOrConstraints === "string") {
|
|
140
|
+
if (!pattern) {
|
|
141
|
+
throw new Error(`Pattern is required for constraint on "${paramOrConstraints}"`);
|
|
142
|
+
}
|
|
143
|
+
const regex = typeof pattern === "string" ? compilePattern(pattern, paramOrConstraints) : pattern;
|
|
144
|
+
addConstraint(paramOrConstraints, regex);
|
|
145
|
+
} else {
|
|
146
|
+
for (const [param, pat] of Object.entries(paramOrConstraints)) {
|
|
147
|
+
const regex = typeof pat === "string" ? compilePattern(pat, param) : pat;
|
|
148
|
+
addConstraint(param, regex);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return builder;
|
|
152
|
+
},
|
|
153
|
+
whereNumber: (param) => {
|
|
154
|
+
addConstraint(param, /^\d+$/);
|
|
155
|
+
return builder;
|
|
156
|
+
},
|
|
157
|
+
whereAlpha: (param) => {
|
|
158
|
+
addConstraint(param, /^[a-zA-Z]+$/);
|
|
159
|
+
return builder;
|
|
160
|
+
},
|
|
161
|
+
whereAlphaNumeric: (param) => {
|
|
162
|
+
addConstraint(param, /^[a-zA-Z0-9]+$/);
|
|
163
|
+
return builder;
|
|
164
|
+
},
|
|
165
|
+
whereUuid: (param) => {
|
|
166
|
+
addConstraint(param, /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
|
167
|
+
return builder;
|
|
168
|
+
},
|
|
169
|
+
whereUlid: (param) => {
|
|
170
|
+
addConstraint(param, /^[0-9A-HJKMNP-TV-Z]{26}$/);
|
|
171
|
+
return builder;
|
|
172
|
+
},
|
|
173
|
+
whereIn: (param, values) => {
|
|
174
|
+
if (values.length === 0) {
|
|
175
|
+
throw new Error(`whereIn requires at least one value for parameter "${param}"`);
|
|
176
|
+
}
|
|
177
|
+
const escaped = values.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
178
|
+
addConstraint(param, new RegExp(`^(${escaped.join("|")})$`));
|
|
179
|
+
return builder;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
return builder;
|
|
183
|
+
}
|
|
184
|
+
function wrapBuilderWithNamePrefix(builder, namePrefix) {
|
|
185
|
+
if (!namePrefix)
|
|
186
|
+
return builder;
|
|
187
|
+
return new Proxy(builder, {
|
|
188
|
+
get(target, prop) {
|
|
189
|
+
if (prop === "name") {
|
|
190
|
+
return (name) => {
|
|
191
|
+
return target.name(namePrefix + name);
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return target[prop];
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
// src/routes/find.ts
|
|
199
|
+
function findRoute(routes, method, path) {
|
|
200
|
+
for (const route of routes) {
|
|
201
|
+
if (route.pattern.test(path)) {
|
|
202
|
+
if (route.method === method) {
|
|
203
|
+
const params = extractParams(path, route);
|
|
204
|
+
if (!checkConstraints(params, route.constraints)) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
return { route, params };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function hasMatchingPath(routes, path) {
|
|
214
|
+
return routes.some((route) => {
|
|
215
|
+
if (!route.pattern.test(path))
|
|
216
|
+
return false;
|
|
217
|
+
const params = extractParams(path, route);
|
|
218
|
+
return checkConstraints(params, route.constraints);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
// src/pathUtils.ts
|
|
222
|
+
function normalizePrefix(prefix) {
|
|
223
|
+
let normalized = prefix;
|
|
224
|
+
if (!normalized.startsWith("/")) {
|
|
225
|
+
normalized = `/${normalized}`;
|
|
226
|
+
}
|
|
227
|
+
if (normalized.endsWith("/") && normalized.length > 1) {
|
|
228
|
+
normalized = normalized.slice(0, -1);
|
|
229
|
+
}
|
|
230
|
+
return normalized;
|
|
231
|
+
}
|
|
232
|
+
function joinPaths(prefix, path) {
|
|
233
|
+
const normalizedPrefix = normalizePrefix(prefix);
|
|
234
|
+
let normalizedPath = path;
|
|
235
|
+
if (!normalizedPath.startsWith("/") && normalizedPath !== "") {
|
|
236
|
+
normalizedPath = `/${normalizedPath}`;
|
|
237
|
+
}
|
|
238
|
+
if (normalizedPath === "/") {
|
|
239
|
+
return normalizedPrefix;
|
|
240
|
+
}
|
|
241
|
+
return normalizedPrefix + normalizedPath;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/routes/group.ts
|
|
245
|
+
function createGroupRouter(prefix, groupMiddleware, namePrefix, addRoute) {
|
|
246
|
+
const router = {
|
|
247
|
+
get: (path, handler) => {
|
|
248
|
+
const fullPath = joinPaths(prefix, path);
|
|
249
|
+
const builder = addRoute("GET", fullPath, handler, groupMiddleware);
|
|
250
|
+
return wrapBuilderWithNamePrefix(builder, namePrefix);
|
|
251
|
+
},
|
|
252
|
+
post: (path, handler) => {
|
|
253
|
+
const fullPath = joinPaths(prefix, path);
|
|
254
|
+
return wrapBuilderWithNamePrefix(addRoute("POST", fullPath, handler, groupMiddleware), namePrefix);
|
|
255
|
+
},
|
|
256
|
+
put: (path, handler) => {
|
|
257
|
+
const fullPath = joinPaths(prefix, path);
|
|
258
|
+
return wrapBuilderWithNamePrefix(addRoute("PUT", fullPath, handler, groupMiddleware), namePrefix);
|
|
259
|
+
},
|
|
260
|
+
delete: (path, handler) => {
|
|
261
|
+
const fullPath = joinPaths(prefix, path);
|
|
262
|
+
return wrapBuilderWithNamePrefix(addRoute("DELETE", fullPath, handler, groupMiddleware), namePrefix);
|
|
263
|
+
},
|
|
264
|
+
patch: (path, handler) => {
|
|
265
|
+
const fullPath = joinPaths(prefix, path);
|
|
266
|
+
return wrapBuilderWithNamePrefix(addRoute("PATCH", fullPath, handler, groupMiddleware), namePrefix);
|
|
267
|
+
},
|
|
268
|
+
group: (prefixOrOptions, callback) => {
|
|
269
|
+
const opts = typeof prefixOrOptions === "string" ? { prefix: prefixOrOptions } : prefixOrOptions;
|
|
270
|
+
const nestedPrefix = joinPaths(prefix, opts.prefix);
|
|
271
|
+
const nestedMiddleware = [...groupMiddleware, ...opts.middleware ?? []];
|
|
272
|
+
const nestedNamePrefix = namePrefix + (opts.name ?? "");
|
|
273
|
+
const nestedRouter = createGroupRouter(nestedPrefix, nestedMiddleware, nestedNamePrefix, addRoute);
|
|
274
|
+
callback(nestedRouter);
|
|
275
|
+
return router;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
return router;
|
|
279
|
+
}
|
|
280
|
+
// src/app.ts
|
|
48
281
|
function createApp() {
|
|
49
282
|
const routes = [];
|
|
50
283
|
const middlewares = [];
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (route.pattern.test(path)) {
|
|
59
|
-
if (route.method === method) {
|
|
60
|
-
return { route, params: extractParams(path, route) };
|
|
61
|
-
}
|
|
62
|
-
}
|
|
284
|
+
const namedRoutes = new Map;
|
|
285
|
+
let globalMiddlewareVersion = 0;
|
|
286
|
+
const middlewareCache = new WeakMap;
|
|
287
|
+
function getMiddlewareChain(route) {
|
|
288
|
+
const cached = middlewareCache.get(route);
|
|
289
|
+
if (cached && cached.version === globalMiddlewareVersion) {
|
|
290
|
+
return cached.chain;
|
|
63
291
|
}
|
|
64
|
-
|
|
292
|
+
const chain = route.middleware ? [...middlewares, ...route.middleware] : middlewares.length > 0 ? [...middlewares] : [];
|
|
293
|
+
middlewareCache.set(route, { version: globalMiddlewareVersion, chain });
|
|
294
|
+
return chain;
|
|
65
295
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
296
|
+
function addRoute(method, path, handler, groupMiddleware = []) {
|
|
297
|
+
const { pattern, paramNames, optionalParams } = compilePath(path);
|
|
298
|
+
const route = {
|
|
299
|
+
method,
|
|
300
|
+
path,
|
|
301
|
+
pattern,
|
|
302
|
+
paramNames,
|
|
303
|
+
handler,
|
|
304
|
+
optionalParams: optionalParams.length > 0 ? optionalParams : undefined,
|
|
305
|
+
middleware: groupMiddleware.length > 0 ? [...groupMiddleware] : undefined
|
|
306
|
+
};
|
|
307
|
+
routes.push(route);
|
|
308
|
+
return createRouteBuilder(route, namedRoutes, app);
|
|
68
309
|
}
|
|
69
310
|
async function handleRequest(request) {
|
|
70
311
|
const url = new URL(request.url);
|
|
71
312
|
const path = url.pathname;
|
|
72
313
|
const method = request.method;
|
|
73
|
-
const match = findRoute(method, path);
|
|
314
|
+
const match = findRoute(routes, method, path);
|
|
74
315
|
if (!match) {
|
|
75
|
-
if (hasMatchingPath(path)) {
|
|
316
|
+
if (hasMatchingPath(routes, path)) {
|
|
76
317
|
return new Response(JSON.stringify({ error: "Method not allowed" }), {
|
|
77
318
|
status: 405,
|
|
78
319
|
headers: { "Content-Type": "application/json" }
|
|
@@ -89,10 +330,11 @@ function createApp() {
|
|
|
89
330
|
query: url.searchParams
|
|
90
331
|
};
|
|
91
332
|
try {
|
|
333
|
+
const allMiddleware = getMiddlewareChain(match.route);
|
|
92
334
|
let index = 0;
|
|
93
335
|
const next = async () => {
|
|
94
|
-
if (index <
|
|
95
|
-
const middleware =
|
|
336
|
+
if (index < allMiddleware.length) {
|
|
337
|
+
const middleware = allMiddleware[index++];
|
|
96
338
|
return await middleware(ctx, next);
|
|
97
339
|
}
|
|
98
340
|
return await match.route.handler(ctx);
|
|
@@ -115,8 +357,67 @@ function createApp() {
|
|
|
115
357
|
patch: (path, handler) => addRoute("PATCH", path, handler),
|
|
116
358
|
use: (middleware) => {
|
|
117
359
|
middlewares.push(middleware);
|
|
360
|
+
globalMiddlewareVersion++;
|
|
361
|
+
return app;
|
|
362
|
+
},
|
|
363
|
+
group: (prefixOrOptions, callback) => {
|
|
364
|
+
const opts = typeof prefixOrOptions === "string" ? { prefix: prefixOrOptions } : prefixOrOptions;
|
|
365
|
+
const groupRouter = createGroupRouter(opts.prefix, opts.middleware ?? [], opts.name ?? "", addRoute);
|
|
366
|
+
callback(groupRouter);
|
|
118
367
|
return app;
|
|
119
368
|
},
|
|
369
|
+
route: (name, params) => {
|
|
370
|
+
const route = namedRoutes.get(name);
|
|
371
|
+
if (!route) {
|
|
372
|
+
throw new Error(`Route "${name}" not found`);
|
|
373
|
+
}
|
|
374
|
+
if (params) {
|
|
375
|
+
for (const [key, value] of Object.entries(params)) {
|
|
376
|
+
const strValue = String(value);
|
|
377
|
+
if (strValue.includes("\r") || strValue.includes(`
|
|
378
|
+
`) || strValue.includes("\x00")) {
|
|
379
|
+
throw new Error(`Invalid character in parameter "${key}": control characters are not allowed`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
let url = route.path;
|
|
384
|
+
const queryParams = {};
|
|
385
|
+
const usedParams = new Set;
|
|
386
|
+
for (const paramName of route.paramNames) {
|
|
387
|
+
const isOptional = route.optionalParams?.includes(paramName);
|
|
388
|
+
const value = params?.[paramName];
|
|
389
|
+
if (value !== undefined) {
|
|
390
|
+
url = url.replace(new RegExp(`:${paramName}\\??`), encodeURIComponent(String(value)));
|
|
391
|
+
usedParams.add(paramName);
|
|
392
|
+
} else if (isOptional) {
|
|
393
|
+
url = url.replace(new RegExp(`/:${paramName}\\?`), "");
|
|
394
|
+
} else {
|
|
395
|
+
throw new Error(`Missing required param "${paramName}" for route "${name}"`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (params) {
|
|
399
|
+
for (const [key, value] of Object.entries(params)) {
|
|
400
|
+
if (!usedParams.has(key)) {
|
|
401
|
+
queryParams[key] = String(value);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (Object.keys(queryParams).length > 0) {
|
|
406
|
+
const qs = new URLSearchParams(queryParams).toString();
|
|
407
|
+
url += `?${qs}`;
|
|
408
|
+
}
|
|
409
|
+
return url;
|
|
410
|
+
},
|
|
411
|
+
hasRoute: (name) => {
|
|
412
|
+
return namedRoutes.has(name);
|
|
413
|
+
},
|
|
414
|
+
getRoutes: () => {
|
|
415
|
+
return routes.map((route) => ({
|
|
416
|
+
name: route.name ?? null,
|
|
417
|
+
method: route.method,
|
|
418
|
+
path: route.path
|
|
419
|
+
}));
|
|
420
|
+
},
|
|
120
421
|
listen: (port = 3000, hostname = "localhost") => {
|
|
121
422
|
const server = Bun.serve({
|
|
122
423
|
port,
|