@rudderjs/router 1.0.0 → 1.1.1
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 +214 -2
- package/boost/guidelines.md +53 -0
- package/dist/index.d.ts +296 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +614 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -39,6 +39,33 @@ router.get('/invoices/:id/download', handler, [ValidateSignature()]).name('invoi
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## Parameter constraints (`where*()`)
|
|
43
|
+
|
|
44
|
+
Constrain `:param` segments to a regex with Laravel-style `where*()` shortcuts. Internally, the path is rewritten to Hono's `:param{regex}` syntax so non-matching requests 404 before they reach the handler.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
router.get('/users/:id', handler).whereNumber('id').name('users.show')
|
|
48
|
+
router.get('/u/:id', handler).whereUuid('id')
|
|
49
|
+
router.get('/posts/:status', handler).whereIn('status', ['draft', 'published'])
|
|
50
|
+
router.get('/n/:n', handler).where('n', /\d{3,5}/)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Method | Pattern |
|
|
54
|
+
|--------|---------|
|
|
55
|
+
| `where(param, regex)` | Custom — string or `RegExp` (uses `.source`) |
|
|
56
|
+
| `whereNumber(param)` | `[0-9]+` |
|
|
57
|
+
| `whereAlpha(param)` | `[A-Za-z]+` |
|
|
58
|
+
| `whereAlphaNumeric(param)` | `[A-Za-z0-9]+` |
|
|
59
|
+
| `whereUuid(param)` | UUID, any version |
|
|
60
|
+
| `whereUlid(param)` | Crockford base32 ULID (26 chars) |
|
|
61
|
+
| `whereIn(param, values)` | Alternation over regex-escaped literals |
|
|
62
|
+
|
|
63
|
+
Chains in any order with `.name()`; calling another `where*()` for the same param overwrites. Throws if the route path has no `:param` segment. The patterns are also exported as constants — `ROUTE_PATTERN_NUMBER`, `ROUTE_PATTERN_ALPHA`, `ROUTE_PATTERN_ALPHANUM`, `ROUTE_PATTERN_UUID`, `ROUTE_PATTERN_ULID`.
|
|
64
|
+
|
|
65
|
+
> Decorator routes (`@Get('/users/:id')`) don't return a `RouteBuilder`, so `where*()` is fluent-only in v1.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
42
69
|
## `route()` — URL generation
|
|
43
70
|
|
|
44
71
|
Generate a URL from a named route. Route parameters are substituted; unused params are appended as a query string.
|
|
@@ -146,6 +173,176 @@ router.post('/admin', handler, [authMiddleware, adminMiddleware])
|
|
|
146
173
|
|
|
147
174
|
---
|
|
148
175
|
|
|
176
|
+
## Route model binding
|
|
177
|
+
|
|
178
|
+
Bind a `:param` segment to any class with a static `findForRoute(value)` method (a `@rudderjs/orm` Model is the canonical fit). Resolution runs as a prepended per-route middleware before your handler, exposing the result as `req.bound!.<name>`.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { router } from '@rudderjs/router'
|
|
182
|
+
import { User } from '../app/Models/User.js'
|
|
183
|
+
|
|
184
|
+
router.bind('user', User)
|
|
185
|
+
|
|
186
|
+
router.get('/users/:user', (req) => {
|
|
187
|
+
const user = req.bound!['user'] // a User instance, or 404 was thrown
|
|
188
|
+
return user
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Optional binding — null instead of 404 when missing
|
|
192
|
+
router.bind('viewer', User, { optional: true })
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Returning `null` from `findForRoute` triggers `RouteModelNotFoundError` (HTTP 404). The raw string remains in `req.params[name]` regardless. Routes whose path doesn't include a bound param are unaffected.
|
|
196
|
+
|
|
197
|
+
The `RouteResolver` contract is duck-typed — `name: string` + `findForRoute(value): unknown | Promise<unknown | null>` — so the router doesn't depend on `@rudderjs/orm`.
|
|
198
|
+
|
|
199
|
+
### Custom 404 with `.missing()`
|
|
200
|
+
|
|
201
|
+
Override the default 404 response per route. Receives the request and the binding error; return a value the route handler may return — `Response`, plain object → JSON, string → body, or `undefined` (you wrote to `res` directly).
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
router.get('/users/:user', show)
|
|
205
|
+
.missing((_req, err) => Response.json({ error: err.message }, { status: 404 }))
|
|
206
|
+
|
|
207
|
+
router.get('/posts/:post', show)
|
|
208
|
+
.missing((_req, err) => ({ message: `Post ${err.value} not found` })) // → 200 JSON
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Optional bindings do NOT trigger `.missing()` — they quietly resolve to `null` instead.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Route groups (`router.group()`)
|
|
216
|
+
|
|
217
|
+
Apply a `prefix`, `domain`, or shared `middleware` stack to every route registered inside the callback. Nested groups concatenate prefixes and middleware; the innermost defined `domain` wins (hosts can't compose).
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { router } from '@rudderjs/router'
|
|
221
|
+
|
|
222
|
+
router.group({ prefix: '/admin', middleware: [adminAuth] }, () => {
|
|
223
|
+
router.get('/users', listUsers) // GET /admin/users (with adminAuth)
|
|
224
|
+
router.get('/posts', listPosts) // GET /admin/posts (with adminAuth)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
router.group({ domain: ':tenant.example.com', prefix: '/api' }, () => {
|
|
228
|
+
router.get('/me', me) // GET :tenant.example.com/api/me
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// Nested
|
|
232
|
+
router.group({ prefix: '/api' }, () => {
|
|
233
|
+
router.group({ prefix: '/v1', middleware: [throttle] }, () => {
|
|
234
|
+
router.get('/users', listUsers) // GET /api/v1/users (with throttle)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`router.group()` is the user-facing scoping primitive. Distinct from `runWithGroup('web' | 'api', …)` — that tags routes with their middleware-group label and is called once by the framework's route loader. Both can be active at the same time.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Subdomain routing (`.domain()`)
|
|
244
|
+
|
|
245
|
+
Restrict a route to a specific host. The template is matched against the request's `Host` header (port stripped, case-insensitive); `:param` segments capture into `req.params` alongside path params.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
router.get('/users', listUsers).domain('api.example.com')
|
|
249
|
+
router.get('/me', me).domain(':tenant.example.com')
|
|
250
|
+
// req.params.tenant === 'acme' for Host: acme.example.com
|
|
251
|
+
|
|
252
|
+
router.group({ domain: 'admin.example.com', middleware: [adminAuth] }, () => {
|
|
253
|
+
router.get('/dashboard', dash) // GET admin.example.com/dashboard
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Mismatched hosts return 404. Subdomain `:param` and path `:param` of the same name collide — path wins.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Resource controllers (`router.resource()`)
|
|
262
|
+
|
|
263
|
+
Wire the seven canonical CRUD routes from a plain controller class — Laravel's `Route::resource` for RudderJS. No decorators; methods are matched by name.
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import { router } from '@rudderjs/router'
|
|
267
|
+
|
|
268
|
+
class PostController {
|
|
269
|
+
async index (_ctx) { /* GET /posts → list */ }
|
|
270
|
+
async create (_ctx) { /* GET /posts/create → form */ }
|
|
271
|
+
async store (_ctx) { /* POST /posts → persist */ }
|
|
272
|
+
async show (_ctx) { /* GET /posts/:post → one */ }
|
|
273
|
+
async edit (_ctx) { /* GET /posts/:post/edit → form */ }
|
|
274
|
+
async update (_ctx) { /* PUT|PATCH /posts/:post → update */ }
|
|
275
|
+
async destroy (_ctx) { /* DELETE /posts/:post → delete */ }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
router.resource('posts', PostController)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
| Verb | Method | Path | Route name |
|
|
282
|
+
|--------|--------|------|------------|
|
|
283
|
+
| index | `GET` | `/posts` | `posts.index` |
|
|
284
|
+
| create | `GET` | `/posts/create` | `posts.create` |
|
|
285
|
+
| store | `POST` | `/posts` | `posts.store` |
|
|
286
|
+
| show | `GET` | `/posts/:post` | `posts.show` |
|
|
287
|
+
| edit | `GET` | `/posts/:post/edit` | `posts.edit` |
|
|
288
|
+
| update | `PUT`+`PATCH` | `/posts/:post` | `posts.update` |
|
|
289
|
+
| destroy | `DELETE` | `/posts/:post` | `posts.destroy` |
|
|
290
|
+
|
|
291
|
+
Methods the controller doesn't implement are silently skipped, so partial controllers work with no `only`/`except` boilerplate.
|
|
292
|
+
|
|
293
|
+
### `apiResource()` — drops the HTML form pages
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
router.apiResource('posts', PostController)
|
|
297
|
+
// no posts.create, no posts.edit
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### `singleton()` — for "the one of these"
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
router.singleton('profile', ProfileController)
|
|
304
|
+
// GET /profile posts.show (named profile.show)
|
|
305
|
+
// GET /profile/edit profile.edit
|
|
306
|
+
// PUT /profile profile.update
|
|
307
|
+
// PATCH /profile (alias)
|
|
308
|
+
|
|
309
|
+
router.singleton('profile', ProfileController).creatable() // adds GET /profile/create + POST /profile
|
|
310
|
+
router.singleton('profile', ProfileController).destroyable() // adds DELETE /profile
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Options
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
router.resource('posts', PostController, {
|
|
317
|
+
only: ['index', 'show'],
|
|
318
|
+
except: ['destroy'],
|
|
319
|
+
parameters: { posts: 'article' }, // /posts/:article instead of /posts/:post
|
|
320
|
+
names: { show: 'posts.detail' },
|
|
321
|
+
middleware: [authMw],
|
|
322
|
+
})
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Per-route customisation via `.builders[]`
|
|
326
|
+
|
|
327
|
+
The registration object exposes the underlying `RouteBuilder[]` — apply `where*()`, additional `.middleware()`, or rename one route in isolation:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
const reg = router.resource('posts', PostController)
|
|
331
|
+
reg.builders[3].whereNumber('post') // constrain show route only
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Builders are in declaration order: `index`, `create`, `store`, `show`, `edit`, `update` (PUT), `update` (PATCH alias), `destroy` — minus any verb the controller skipped.
|
|
335
|
+
|
|
336
|
+
### Scaffolder
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
pnpm rudder make:controller PostController --resource # full 7-verb stub
|
|
340
|
+
pnpm rudder make:controller PostController --api # API-only (no create/edit)
|
|
341
|
+
pnpm rudder make:controller ProfileController --singleton
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
149
346
|
## Mounting onto a server adapter
|
|
150
347
|
|
|
151
348
|
```ts
|
|
@@ -169,20 +366,35 @@ router.mount(serverAdapter)
|
|
|
169
366
|
| `all(path, handler, mw?)` | `RouteBuilder` | Register route matching any method |
|
|
170
367
|
| `add(method, path, handler, mw?)` | `this` | Register route with explicit method |
|
|
171
368
|
| `use(middleware)` | `this` | Register global middleware |
|
|
369
|
+
| `bind(name, resolver, opts?)` | `this` | Bind a `:param` to a `RouteResolver` (e.g. an ORM Model) for auto-resolution |
|
|
370
|
+
| `listBindings()` | `Record<string, RouteResolver>` | All registered route bindings |
|
|
371
|
+
| `group(opts, fn)` | `this` | Apply prefix/domain/middleware to every route registered inside `fn` |
|
|
372
|
+
| `resource(name, Ctrl, opts?)` | `ResourceRegistration` | Register the seven canonical RESTful routes |
|
|
373
|
+
| `apiResource(name, Ctrl, opts?)` | `ResourceRegistration` | Resource minus `create`/`edit` |
|
|
374
|
+
| `singleton(name, Ctrl, opts?)` | `SingletonRegistration` | `show`/`edit`/`update` for a single-instance resource |
|
|
172
375
|
| `registerController(Class)` | `this` | Register decorator-based controller |
|
|
173
376
|
| `mount(serverAdapter)` | `void` | Apply middleware + routes to adapter |
|
|
174
377
|
| `list()` | `RouteDefinition[]` | All registered routes |
|
|
175
378
|
| `listNamed()` | `Record<string, string>` | All named routes |
|
|
176
379
|
| `getNamedRoute(name)` | `string \| undefined` | Path for a named route |
|
|
177
|
-
| `reset()` | `this` | Clear routes, middleware,
|
|
380
|
+
| `reset()` | `this` | Clear routes, middleware, named routes, and bindings |
|
|
178
381
|
|
|
179
382
|
### `RouteBuilder`
|
|
180
383
|
|
|
181
|
-
Returned by the shorthand route methods. Allows naming the registered route.
|
|
384
|
+
Returned by the shorthand route methods. Allows naming the registered route and constraining `:param` segments.
|
|
182
385
|
|
|
183
386
|
| Method | Description |
|
|
184
387
|
|--------|-------------|
|
|
185
388
|
| `.name(n)` | Assign a name to the route |
|
|
389
|
+
| `.where(param, regex)` | Constrain `:param` to a custom regex (string or `RegExp`) |
|
|
390
|
+
| `.whereNumber(param)` | Shortcut for `[0-9]+` |
|
|
391
|
+
| `.whereAlpha(param)` | Shortcut for `[A-Za-z]+` |
|
|
392
|
+
| `.whereAlphaNumeric(param)` | Shortcut for `[A-Za-z0-9]+` |
|
|
393
|
+
| `.whereUuid(param)` | Shortcut for any-version UUID |
|
|
394
|
+
| `.whereUlid(param)` | Shortcut for Crockford base32 ULID |
|
|
395
|
+
| `.whereIn(param, values)` | Constrain `:param` to one of the supplied literals |
|
|
396
|
+
| `.domain(template)` | Restrict to a host; `:param` segments capture into `req.params` |
|
|
397
|
+
| `.missing(fn)` | Custom 404 callback when an explicit binding fails to resolve |
|
|
186
398
|
|
|
187
399
|
### `Url`
|
|
188
400
|
|
package/boost/guidelines.md
CHANGED
|
@@ -34,6 +34,59 @@ route('posts.show', { slug: 'hello' }) // optional ':id?' segment omitted
|
|
|
34
34
|
|
|
35
35
|
Throws if a required parameter is missing or the name is not registered.
|
|
36
36
|
|
|
37
|
+
### Parameter constraints
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { Route } from '@rudderjs/router'
|
|
41
|
+
|
|
42
|
+
Route.get('/users/:id', handler).whereNumber('id').name('users.show')
|
|
43
|
+
Route.get('/u/:id', handler).whereUuid('id')
|
|
44
|
+
Route.get('/posts/:status', handler).whereIn('status', ['draft', 'published'])
|
|
45
|
+
Route.get('/n/:n', handler).where('n', /\d{3,5}/)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Available shortcuts: `whereNumber` / `whereAlpha` / `whereAlphaNumeric` / `whereUuid` / `whereUlid` / `whereIn(param, values)`. Base method `.where(param, regex)` accepts a string or `RegExp`. Throws when the path has no `:param` segment, or when `whereIn` gets an empty values array. Order-independent against `.name()`.
|
|
49
|
+
|
|
50
|
+
> Fluent-only — decorator routes (`@Get('/users/:id')`) don't return a `RouteBuilder`.
|
|
51
|
+
|
|
52
|
+
### Route groups
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { router } from '@rudderjs/router'
|
|
56
|
+
|
|
57
|
+
router.group({ prefix: '/admin', middleware: [adminAuth] }, () => {
|
|
58
|
+
router.get('/users', listUsers) // GET /admin/users (with adminAuth)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
router.group({ domain: ':tenant.example.com', prefix: '/api' }, () => {
|
|
62
|
+
router.get('/me', me) // GET :tenant.example.com/api/me
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Nested groups concatenate prefixes and middleware; innermost defined `domain` wins. `router.group()` is the user-facing scoping primitive — distinct from `runWithGroup('web' | 'api', …)` (the framework's web/api middleware-group tag).
|
|
67
|
+
|
|
68
|
+
### Subdomain routing
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
router.get('/users', listUsers).domain('api.example.com')
|
|
72
|
+
router.get('/me', me).domain(':tenant.example.com')
|
|
73
|
+
// req.params.tenant === 'acme' for Host: acme.example.com
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Mismatched hosts return 404. Subdomain `:param` and path `:param` of the same name collide — path wins.
|
|
77
|
+
|
|
78
|
+
### Route binding 404 customisation
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
router.get('/users/:user', show)
|
|
82
|
+
.missing((_req, err) => Response.json({ error: err.message }, { status: 404 }))
|
|
83
|
+
|
|
84
|
+
router.get('/posts/:post', show)
|
|
85
|
+
.missing((_req, err) => ({ message: `Post ${err.value} not found` }))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Returns: `Response`, plain object → JSON, string → body, or `undefined` (callback wrote to `res` directly). Optional bindings do NOT trigger `.missing()`.
|
|
89
|
+
|
|
37
90
|
### Route-level middleware
|
|
38
91
|
|
|
39
92
|
```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -12,11 +12,22 @@ export declare const Put: (path?: string) => MethodDecorator;
|
|
|
12
12
|
export declare const Patch: (path?: string) => MethodDecorator;
|
|
13
13
|
export declare const Delete: (path?: string) => MethodDecorator;
|
|
14
14
|
export declare const Options: (path?: string) => MethodDecorator;
|
|
15
|
+
/** Matches one or more digits — `[0-9]+`. */
|
|
16
|
+
export declare const ROUTE_PATTERN_NUMBER = "[0-9]+";
|
|
17
|
+
/** Matches one or more ASCII letters — `[A-Za-z]+`. */
|
|
18
|
+
export declare const ROUTE_PATTERN_ALPHA = "[A-Za-z]+";
|
|
19
|
+
/** Matches one or more ASCII letters or digits — `[A-Za-z0-9]+`. */
|
|
20
|
+
export declare const ROUTE_PATTERN_ALPHANUM = "[A-Za-z0-9]+";
|
|
21
|
+
/** Matches a UUID of any version (case-insensitive). */
|
|
22
|
+
export declare const ROUTE_PATTERN_UUID = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
|
23
|
+
/** Matches a Crockford base32 ULID (26 chars). */
|
|
24
|
+
export declare const ROUTE_PATTERN_ULID = "[0-7][0-9A-HJKMNP-TV-Z]{25}";
|
|
15
25
|
/**
|
|
16
|
-
* Returned by `router.get/post/etc` — allows naming the registered route
|
|
26
|
+
* Returned by `router.get/post/etc` — allows naming the registered route and
|
|
27
|
+
* constraining `:param` segments via Laravel-style `where*` shortcuts.
|
|
17
28
|
*
|
|
18
29
|
* @example
|
|
19
|
-
* router.get('/users/:id', handler).name('users.show')
|
|
30
|
+
* router.get('/users/:id', handler).name('users.show').whereNumber('id')
|
|
20
31
|
* route('users.show', { id: 1 }) // → '/users/1'
|
|
21
32
|
*/
|
|
22
33
|
export declare class RouteBuilder {
|
|
@@ -25,14 +36,125 @@ export declare class RouteBuilder {
|
|
|
25
36
|
constructor(definition: RouteDefinition, _router: Router);
|
|
26
37
|
/** Assign a name to this route for use with `route()` and `Url.signedRoute()`. */
|
|
27
38
|
name(n: string): this;
|
|
39
|
+
/**
|
|
40
|
+
* Constrain a `:param` segment with a custom regex. Accepts either a string
|
|
41
|
+
* (used verbatim) or a `RegExp` (its `.source` is taken — flags are dropped
|
|
42
|
+
* automatically; anchors `^` / `$` pass through but are typically redundant
|
|
43
|
+
* since Hono anchors per-segment).
|
|
44
|
+
*
|
|
45
|
+
* Mutates the route's path in place to `:param{pattern}` (Hono regex syntax).
|
|
46
|
+
* Calling `where*` again on the same param overwrites the previous pattern.
|
|
47
|
+
*
|
|
48
|
+
* Throws if the route path has no `:param` segment.
|
|
49
|
+
*/
|
|
50
|
+
where(param: string, regex: string | RegExp): this;
|
|
51
|
+
/** Constrain `:param` to one or more digits. */
|
|
52
|
+
whereNumber(param: string): this;
|
|
53
|
+
/** Constrain `:param` to one or more ASCII letters. */
|
|
54
|
+
whereAlpha(param: string): this;
|
|
55
|
+
/** Constrain `:param` to one or more ASCII letters or digits. */
|
|
56
|
+
whereAlphaNumeric(param: string): this;
|
|
57
|
+
/** Constrain `:param` to a UUID of any version. */
|
|
58
|
+
whereUuid(param: string): this;
|
|
59
|
+
/** Constrain `:param` to a Crockford base32 ULID. */
|
|
60
|
+
whereUlid(param: string): this;
|
|
61
|
+
/**
|
|
62
|
+
* Constrain `:param` to one of the supplied literal values. Each value is
|
|
63
|
+
* regex-escaped, so `'a.b'` matches the literal string `a.b`, not "a then any
|
|
64
|
+
* char then b". Throws when `values` is empty.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* router.get('/posts/:status', handler).whereIn('status', ['draft', 'published'])
|
|
68
|
+
*/
|
|
69
|
+
whereIn(param: string, values: readonly (string | number)[]): this;
|
|
70
|
+
/**
|
|
71
|
+
* Restrict this route to a specific subdomain. The template is matched against
|
|
72
|
+
* the request's `Host` header (port stripped, case-insensitive); `:param`
|
|
73
|
+
* segments capture into `req.params` alongside path params.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* router.get('/users', listUsers).domain('api.example.com')
|
|
77
|
+
* router.get('/admin', dash).domain(':tenant.example.com') // captures req.params.tenant
|
|
78
|
+
*/
|
|
79
|
+
domain(template: string): this;
|
|
80
|
+
/**
|
|
81
|
+
* Custom response when an explicit route binding (`router.bind('user', User)`)
|
|
82
|
+
* resolves to `null`. Receives the request and the not-found error; return any
|
|
83
|
+
* value a handler may return (`Response`, plain object → JSON, string → body,
|
|
84
|
+
* or `undefined` after writing to `res` directly). Does not fire for optional
|
|
85
|
+
* bindings — those quietly resolve to `null` instead.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* router.get('/users/:user', show)
|
|
89
|
+
* .missing((_req, err) => Response.json({ error: err.message }, { status: 404 }))
|
|
90
|
+
*/
|
|
91
|
+
missing(fn: NonNullable<RouteDefinition['missing']>): this;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Options accepted by `router.group(opts, fn)`. Each route registered inside
|
|
95
|
+
* `fn` inherits the prefix, domain, and middleware. Nested groups concatenate
|
|
96
|
+
* prefixes and middleware; the innermost defined `domain` wins (hosts can't
|
|
97
|
+
* compose). Distinct from `runWithGroup('web' | 'api', …)`, which only tags
|
|
98
|
+
* routes with their middleware-group label.
|
|
99
|
+
*/
|
|
100
|
+
export interface RouteGroupOptions {
|
|
101
|
+
/** Path prefix applied to every route registered in the group. */
|
|
102
|
+
prefix?: string;
|
|
103
|
+
/** Subdomain template applied to every route registered in the group. */
|
|
104
|
+
domain?: string;
|
|
105
|
+
/** Middleware prepended to every route's chain (before per-route middleware). */
|
|
106
|
+
middleware?: MiddlewareHandler[];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Duck-typed contract for any object that resolves a string route parameter
|
|
110
|
+
* into a value (typically a Model instance, but the router doesn't depend on
|
|
111
|
+
* `@rudderjs/orm` — anything with a static `findForRoute` method works).
|
|
112
|
+
*
|
|
113
|
+
* Returning `null` signals "not found" — the router maps that to a thrown
|
|
114
|
+
* `RouteModelNotFoundError`, which the framework's HTTP layer renders as a 404.
|
|
115
|
+
*/
|
|
116
|
+
export interface RouteResolver {
|
|
117
|
+
/** Owning class name — used for error messages only. */
|
|
118
|
+
name: string;
|
|
119
|
+
/** Resolve the raw param value. Return `null` for not-found. */
|
|
120
|
+
findForRoute(value: string): Promise<unknown | null> | unknown | null;
|
|
121
|
+
}
|
|
122
|
+
export interface RouteBindingOptions {
|
|
123
|
+
/**
|
|
124
|
+
* When `true`, an absent or unresolvable param value silently sets
|
|
125
|
+
* `req.bound[name] = null` instead of throwing. Useful for shared routes
|
|
126
|
+
* that may or may not have a logged-in subject.
|
|
127
|
+
*/
|
|
128
|
+
optional?: boolean;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Thrown by route binding middleware when a required `{param}` cannot be
|
|
132
|
+
* resolved into a model instance. `@rudderjs/core` picks up the duck-typed
|
|
133
|
+
* `httpStatus` and renders this as an HTTP 404; apps can catch it explicitly
|
|
134
|
+
* to render a custom not-found page.
|
|
135
|
+
*/
|
|
136
|
+
export declare class RouteModelNotFoundError extends Error {
|
|
137
|
+
readonly model: string;
|
|
138
|
+
readonly param: string;
|
|
139
|
+
readonly value: string;
|
|
140
|
+
/** Duck-typed signal to `@rudderjs/core`'s exception handler. */
|
|
141
|
+
readonly httpStatus = 404;
|
|
142
|
+
constructor(model: string, param: string, value: string);
|
|
28
143
|
}
|
|
29
144
|
export declare class Router {
|
|
30
145
|
private routes;
|
|
31
146
|
private globalMiddleware;
|
|
32
147
|
private namedRoutes;
|
|
148
|
+
private bindings;
|
|
149
|
+
/**
|
|
150
|
+
* Active `group()` scopes, outermost first. Synchronous module-level state
|
|
151
|
+
* is fine — route loaders execute synchronously at module evaluation, and
|
|
152
|
+
* `group()` only mutates the stack inside its own callback's lifetime.
|
|
153
|
+
*/
|
|
154
|
+
private _groupStack;
|
|
33
155
|
/** @internal — called by RouteBuilder */
|
|
34
|
-
_registerName(name: string,
|
|
35
|
-
/** Look up a named route's path. */
|
|
156
|
+
_registerName(name: string, def: RouteDefinition): void;
|
|
157
|
+
/** Look up a named route's path. Reflects any `where*()` mutations. */
|
|
36
158
|
getNamedRoute(name: string): string | undefined;
|
|
37
159
|
/**
|
|
38
160
|
* Check whether a named route is registered.
|
|
@@ -47,10 +169,77 @@ export declare class Router {
|
|
|
47
169
|
has(name: string): boolean;
|
|
48
170
|
/** All registered named routes. */
|
|
49
171
|
listNamed(): Record<string, string>;
|
|
50
|
-
/** Clear registered routes, middleware, and
|
|
172
|
+
/** Clear registered routes, middleware, named routes, and route bindings. */
|
|
51
173
|
reset(): this;
|
|
174
|
+
/**
|
|
175
|
+
* Run `fn` with a group scope active. Every route registered (via fluent
|
|
176
|
+
* `.get()`/`.post()`/etc. or `registerController()`) inside `fn` inherits
|
|
177
|
+
* the group's `prefix`, `domain`, and `middleware`. Nested calls compose:
|
|
178
|
+
* prefixes concatenate, middleware stacks accumulate, the innermost defined
|
|
179
|
+
* `domain` wins.
|
|
180
|
+
*
|
|
181
|
+
* Distinct from `runWithGroup('web' | 'api', …)` — that tags routes with
|
|
182
|
+
* their middleware-group label (web vs api) and is called once by the
|
|
183
|
+
* framework's route loader. `router.group()` is the user-facing scoping
|
|
184
|
+
* primitive; both can be active at the same time.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* router.group({ prefix: '/admin', middleware: [adminAuth] }, () => {
|
|
188
|
+
* router.get('/users', listUsers) // GET /admin/users (with adminAuth)
|
|
189
|
+
* router.get('/posts', listPosts) // GET /admin/posts (with adminAuth)
|
|
190
|
+
* })
|
|
191
|
+
*
|
|
192
|
+
* router.group({ domain: ':tenant.example.com', prefix: '/api' }, () => {
|
|
193
|
+
* router.get('/me', me) // GET :tenant.example.com/api/me
|
|
194
|
+
* })
|
|
195
|
+
*/
|
|
196
|
+
group(opts: RouteGroupOptions, fn: () => void): this;
|
|
197
|
+
/**
|
|
198
|
+
* Compose the active group stack into the values used to register a route.
|
|
199
|
+
* Path prefixes concatenate (and collapse `/+` to `/`), middleware stacks
|
|
200
|
+
* concatenate, the innermost defined `host` wins.
|
|
201
|
+
*/
|
|
202
|
+
private _applyGroupStack;
|
|
52
203
|
/** Register a global middleware (runs on every route). */
|
|
53
204
|
use(middleware: MiddlewareHandler): this;
|
|
205
|
+
/**
|
|
206
|
+
* Bind a route parameter name to a resolver. When a route's path contains
|
|
207
|
+
* `:<name>`, the matching string param is resolved before the handler runs;
|
|
208
|
+
* the result is exposed as `req.bound[name]`. The raw string remains in
|
|
209
|
+
* `req.params[name]` so existing code keeps working.
|
|
210
|
+
*
|
|
211
|
+
* Resolvers are duck-typed — pass any class with a static `findForRoute(val)`
|
|
212
|
+
* method (`@rudderjs/orm` Model classes match by default). Bindings are
|
|
213
|
+
* opt-in: routes whose path does not include the bound `:name` are unaffected.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* import { router } from '@rudderjs/router'
|
|
217
|
+
* import { User } from '../app/Models/User.js'
|
|
218
|
+
*
|
|
219
|
+
* router.bind('user', User)
|
|
220
|
+
* router.get('/users/:user', (req) => req.bound!['user'])
|
|
221
|
+
*
|
|
222
|
+
* // Custom column → declare on the model:
|
|
223
|
+
* class Post extends Model {
|
|
224
|
+
* static override routeKey = 'slug'
|
|
225
|
+
* }
|
|
226
|
+
* router.bind('post', Post) // resolves /posts/:post by slug
|
|
227
|
+
*
|
|
228
|
+
* // Optional binding — null when missing instead of 404:
|
|
229
|
+
* router.bind('viewer', User, { optional: true })
|
|
230
|
+
*/
|
|
231
|
+
bind(name: string, resolver: RouteResolver, options?: RouteBindingOptions): this;
|
|
232
|
+
/** All registered route bindings, keyed by param name. */
|
|
233
|
+
listBindings(): Record<string, RouteResolver>;
|
|
234
|
+
/**
|
|
235
|
+
* Build the per-route binding middleware. Walks the route's `:param` segments,
|
|
236
|
+
* looks them up in the binding map, and resolves each before calling `next()`.
|
|
237
|
+
* No-op for routes whose path contains no bound params.
|
|
238
|
+
*
|
|
239
|
+
* Takes the full `RouteDefinition` (not just path) so the closure can capture
|
|
240
|
+
* `def.missing` — the per-route 404 customisation set via `RouteBuilder.missing()`.
|
|
241
|
+
*/
|
|
242
|
+
private _buildBindingMiddleware;
|
|
54
243
|
/** Manually register a route. Returns `this` for bulk registration. */
|
|
55
244
|
add(method: HttpMethod, path: string, handler: RouteHandler, middleware?: MiddlewareHandler[]): this;
|
|
56
245
|
get(path: string, handler: RouteHandler, middleware?: MiddlewareHandler[]): RouteBuilder;
|
|
@@ -66,6 +255,107 @@ export declare class Router {
|
|
|
66
255
|
mount(server: ServerAdapter): void;
|
|
67
256
|
/** All registered routes — useful for `routes:list`. */
|
|
68
257
|
list(): RouteDefinition[];
|
|
258
|
+
/**
|
|
259
|
+
* Register the canonical seven CRUD routes for a plain controller class:
|
|
260
|
+
* `index`, `create`, `store`, `show`, `edit`, `update`, `destroy`. Methods
|
|
261
|
+
* the controller doesn't implement are silently skipped, so a partial
|
|
262
|
+
* controller works without `only`/`except` boilerplate.
|
|
263
|
+
*
|
|
264
|
+
* The `update` route is registered for both `PUT` and `PATCH`. Route names
|
|
265
|
+
* default to `<name>.<verb>` (`posts.show`, `posts.update`). The path param
|
|
266
|
+
* defaults to a naive singular (`posts` → `:post`); pass
|
|
267
|
+
* `{ parameters: { posts: 'article' } }` to override.
|
|
268
|
+
*
|
|
269
|
+
* Use plain method names — no decorators. Call `router.registerController()`
|
|
270
|
+
* for decorator-driven controllers instead.
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* router.resource('posts', PostController)
|
|
274
|
+
* router.resource('posts', PostController, { only: ['index', 'show'] })
|
|
275
|
+
* router.resource('posts', PostController, { middleware: [authMw] })
|
|
276
|
+
*/
|
|
277
|
+
resource(name: string, Ctrl: new () => object, opts?: ResourceOptions): ResourceRegistration;
|
|
278
|
+
/**
|
|
279
|
+
* Register an API-only resource — the same routes as `resource()` minus
|
|
280
|
+
* `create` and `edit`, since those render HTML forms and have no JSON
|
|
281
|
+
* equivalent.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* router.apiResource('posts', PostController)
|
|
285
|
+
*/
|
|
286
|
+
apiResource(name: string, Ctrl: new () => object, opts?: ResourceOptions): ResourceRegistration;
|
|
287
|
+
/**
|
|
288
|
+
* Register a singleton resource — `show`, `edit`, `update` only. Use for
|
|
289
|
+
* "the current user's profile" / "the application's settings" style
|
|
290
|
+
* resources where there's only ever one of the thing.
|
|
291
|
+
*
|
|
292
|
+
* Add a creation flow with `.creatable()` (registers `create` + `store`) or
|
|
293
|
+
* a deletion flow with `.destroyable()` (registers `destroy`).
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* router.singleton('profile', ProfileController) // /profile + /profile/edit
|
|
297
|
+
* router.singleton('profile', ProfileController).creatable() // also /profile/create + POST /profile
|
|
298
|
+
*/
|
|
299
|
+
singleton(name: string, Ctrl: new () => object, opts?: ResourceOptions): SingletonRegistration;
|
|
300
|
+
/** @internal — shared registration loop for resource/apiResource/singleton. */
|
|
301
|
+
_registerResource(name: string, Ctrl: new () => object, table: readonly ResourceVerbSpec[], opts: ResourceOptions): ResourceRegistration;
|
|
302
|
+
}
|
|
303
|
+
/** The seven canonical RESTful verbs Laravel's `Route::resource` exposes. */
|
|
304
|
+
export type ResourceVerb = 'index' | 'create' | 'store' | 'show' | 'edit' | 'update' | 'destroy';
|
|
305
|
+
interface ResourceVerbSpec {
|
|
306
|
+
verb: ResourceVerb;
|
|
307
|
+
method: HttpMethod;
|
|
308
|
+
path: (name: string, param: string) => string;
|
|
309
|
+
nameSuffix: string;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Options accepted by `router.resource`/`apiResource`/`singleton`.
|
|
313
|
+
*
|
|
314
|
+
* - `only`/`except` — restrict the verbs registered.
|
|
315
|
+
* - `parameters` — override the `:param` segment name for a given resource
|
|
316
|
+
* (e.g. `{ posts: 'article' }` → `/posts/:article`).
|
|
317
|
+
* - `names` — override the generated route names per verb.
|
|
318
|
+
* - `middleware` — applied to every route registered by the resource.
|
|
319
|
+
*/
|
|
320
|
+
export interface ResourceOptions {
|
|
321
|
+
only?: readonly ResourceVerb[];
|
|
322
|
+
except?: readonly ResourceVerb[];
|
|
323
|
+
parameters?: Record<string, string>;
|
|
324
|
+
names?: Partial<Record<ResourceVerb, string>>;
|
|
325
|
+
middleware?: MiddlewareHandler[];
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Returned by `router.resource()`/`apiResource()`. The `builders` array holds
|
|
329
|
+
* one `RouteBuilder` per registered route in declaration order — apply
|
|
330
|
+
* `where*()`, additional middleware, or rename individual routes by indexing
|
|
331
|
+
* directly. The `update` PATCH alias is included as a separate builder
|
|
332
|
+
* immediately after its PUT counterpart.
|
|
333
|
+
*/
|
|
334
|
+
export declare class ResourceRegistration {
|
|
335
|
+
readonly builders: RouteBuilder[];
|
|
336
|
+
constructor(builders: RouteBuilder[]);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Returned by `router.singleton()`. Adds two opt-in helpers on top of
|
|
340
|
+
* `ResourceRegistration` for resources that also expose a creation flow
|
|
341
|
+
* (`.creatable()`) or deletion flow (`.destroyable()`).
|
|
342
|
+
*/
|
|
343
|
+
export declare class SingletonRegistration extends ResourceRegistration {
|
|
344
|
+
private readonly _router;
|
|
345
|
+
private readonly _name;
|
|
346
|
+
private readonly _Ctrl;
|
|
347
|
+
private readonly _opts;
|
|
348
|
+
constructor(builders: RouteBuilder[], _router: Router, _name: string, _Ctrl: new () => object, _opts: ResourceOptions);
|
|
349
|
+
/**
|
|
350
|
+
* Add `GET /<name>/create` and `POST /<name>` — the create/store half of a
|
|
351
|
+
* full resource. Skipped for any verb the controller doesn't implement.
|
|
352
|
+
*/
|
|
353
|
+
creatable(): this;
|
|
354
|
+
/**
|
|
355
|
+
* Add `DELETE /<name>` — the destroy half of a full resource. Skipped if
|
|
356
|
+
* the controller doesn't implement `destroy()`.
|
|
357
|
+
*/
|
|
358
|
+
destroyable(): this;
|
|
69
359
|
}
|
|
70
360
|
export declare const router: Router;
|
|
71
361
|
/** Alias for router — Laravel-style capitalised name */
|
|
@@ -126,4 +416,5 @@ export declare class Url {
|
|
|
126
416
|
* router.get('/invoice/:id/download', handler, [ValidateSignature()])
|
|
127
417
|
*/
|
|
128
418
|
export declare function ValidateSignature(): MiddlewareHandler;
|
|
419
|
+
export {};
|
|
129
420
|
//# sourceMappingURL=index.d.ts.map
|