@c9up/ream 0.1.24 → 0.1.26
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/dist/ConfigLoader.d.ts +46 -5
- package/dist/ConfigLoader.d.ts.map +1 -1
- package/dist/ConfigLoader.js +60 -9
- package/dist/ConfigLoader.js.map +1 -1
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +46 -6
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +21 -3
- package/dist/Ignitor.js.map +1 -1
- package/dist/container/Container.d.ts +23 -3
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +139 -60
- package/dist/container/Container.js.map +1 -1
- package/dist/env/Env.d.ts +29 -4
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +27 -4
- package/dist/env/Env.js.map +1 -1
- package/dist/env/Secret.d.ts +11 -0
- package/dist/env/Secret.d.ts.map +1 -0
- package/dist/env/Secret.js +36 -0
- package/dist/env/Secret.js.map +1 -0
- package/dist/env/interpolate.d.ts +26 -0
- package/dist/env/interpolate.d.ts.map +1 -0
- package/dist/env/interpolate.js +48 -0
- package/dist/env/interpolate.js.map +1 -0
- package/dist/env/loadEnvFiles.d.ts.map +1 -1
- package/dist/env/loadEnvFiles.js +14 -4
- package/dist/env/loadEnvFiles.js.map +1 -1
- package/dist/env/schema.d.ts +23 -6
- package/dist/env/schema.d.ts.map +1 -1
- package/dist/env/schema.js +67 -33
- package/dist/env/schema.js.map +1 -1
- package/dist/events/Emitter.d.ts +1 -1
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +1 -1
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/EventsProvider.d.ts +2 -2
- package/dist/events/EventsProvider.d.ts.map +1 -1
- package/dist/events/EventsProvider.js +3 -3
- package/dist/events/EventsProvider.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +1 -1
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +2 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/graphql/GraphQLProvider.d.ts.map +1 -1
- package/dist/graphql/GraphQLProvider.js +7 -3
- package/dist/graphql/GraphQLProvider.js.map +1 -1
- package/dist/http/Exception.d.ts +63 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +185 -48
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/HttpContext.d.ts +56 -9
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +56 -15
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +4 -1
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts +141 -10
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +244 -34
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +176 -9
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +431 -19
- package/dist/http/Response.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.js +1 -1
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/openapi/OpenApiProvider.js +4 -4
- package/dist/openapi/OpenApiProvider.js.map +1 -1
- package/dist/router/Router.d.ts +223 -31
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +520 -102
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcProvider.d.ts +1 -0
- package/dist/rpc/RpcProvider.d.ts.map +1 -1
- package/dist/rpc/RpcProvider.js +7 -3
- package/dist/rpc/RpcProvider.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +2 -2
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +5 -4
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
- package/dist/scheduler/ScheduleProvider.js +7 -15
- package/dist/scheduler/ScheduleProvider.js.map +1 -1
- package/dist/server/Server.d.ts +17 -4
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +41 -7
- package/dist/server/Server.js.map +1 -1
- package/dist/session/SessionMiddleware.js +3 -3
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/storage/StaticProvider.js +1 -1
- package/dist/storage/StaticProvider.js.map +1 -1
- package/dist/testing/RequestBuilder.d.ts +60 -0
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +180 -1
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +22 -1
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +40 -0
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +30 -0
- package/dist/utils/Macroable.d.ts.map +1 -0
- package/dist/utils/Macroable.js +43 -0
- package/dist/utils/Macroable.js.map +1 -0
- package/dist/utils/inflect.d.ts +17 -0
- package/dist/utils/inflect.d.ts.map +1 -0
- package/dist/utils/inflect.js +32 -0
- package/dist/utils/inflect.js.map +1 -0
- package/dist/utils/objectPath.d.ts +57 -0
- package/dist/utils/objectPath.d.ts.map +1 -0
- package/dist/utils/objectPath.js +141 -0
- package/dist/utils/objectPath.js.map +1 -0
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +6 -1
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/ConfigLoader.ts +72 -9
- package/src/HttpKernel.ts +53 -15
- package/src/Ignitor.ts +24 -3
- package/src/container/Container.ts +145 -61
- package/src/env/Env.ts +49 -5
- package/src/env/Secret.ts +41 -0
- package/src/env/interpolate.ts +54 -0
- package/src/env/loadEnvFiles.ts +12 -4
- package/src/env/schema.ts +109 -35
- package/src/events/Emitter.ts +2 -2
- package/src/events/EventsProvider.ts +5 -5
- package/src/graphql/GraphQLEngine.ts +4 -4
- package/src/graphql/GraphQLProvider.ts +7 -3
- package/src/http/Exception.ts +214 -37
- package/src/http/HttpContext.ts +93 -20
- package/src/http/RedirectBuilder.ts +4 -1
- package/src/http/Request.ts +264 -34
- package/src/http/Response.ts +504 -30
- package/src/index.ts +11 -2
- package/src/middleware/Pipeline.ts +1 -1
- package/src/openapi/OpenApiProvider.ts +4 -4
- package/src/router/Router.ts +631 -113
- package/src/rpc/RpcProvider.ts +6 -2
- package/src/rpc/RpcRouter.ts +7 -6
- package/src/scheduler/ScheduleProvider.ts +7 -15
- package/src/server/Server.ts +49 -10
- package/src/session/SessionMiddleware.ts +4 -4
- package/src/storage/StaticProvider.ts +1 -1
- package/src/testing/RequestBuilder.ts +218 -1
- package/src/testing/TestClient.ts +57 -1
- package/src/utils/Macroable.ts +54 -0
- package/src/utils/inflect.ts +29 -0
- package/src/utils/objectPath.ts +142 -0
- package/dist/decorators/Lazy.d.ts +0 -19
- package/dist/decorators/Lazy.d.ts.map +0 -1
- package/dist/decorators/Lazy.js +0 -53
- package/dist/decorators/Lazy.js.map +0 -1
- package/src/decorators/Lazy.ts +0 -57
package/src/router/Router.ts
CHANGED
|
@@ -7,8 +7,21 @@
|
|
|
7
7
|
import type { HttpContext } from '../http/HttpContext.js'
|
|
8
8
|
import { safeDecodeURIComponent } from '../http/urlDecode.js'
|
|
9
9
|
import type { MiddlewareFunction } from '../middleware/Pipeline.js'
|
|
10
|
+
import type { SignedUrl } from '../security/SignedUrl.js'
|
|
10
11
|
import type { MiddlewareEntry } from '../server/Server.js'
|
|
11
|
-
import { resolveMiddlewareEntry } from '../server/Server.js'
|
|
12
|
+
import { resolveMiddlewareEntry, resolveParametrizedMiddlewareEntry } from '../server/Server.js'
|
|
13
|
+
import { singular, snakeCase } from '../utils/inflect.js'
|
|
14
|
+
import { Macroable } from '../utils/Macroable.js'
|
|
15
|
+
|
|
16
|
+
/** Options for {@link Router.makeSignedUrl}. */
|
|
17
|
+
export interface SignedUrlOptions {
|
|
18
|
+
/** Extra query-string params folded into the signature. */
|
|
19
|
+
qs?: Record<string, string>
|
|
20
|
+
/** Lifetime before the link expires — seconds (number) or a `'30m'`/`'2h'`/`'1d'` string. */
|
|
21
|
+
expiresIn?: string | number
|
|
22
|
+
/** Namespaces the signature so a URL signed for one purpose can't be reused for another. */
|
|
23
|
+
purpose?: string
|
|
24
|
+
}
|
|
12
25
|
|
|
13
26
|
// ─── Types ──────────────────────────────────────────────────
|
|
14
27
|
|
|
@@ -22,8 +35,21 @@ export type RouteHandlerFunction = (ctx: HttpContext) => Promise<void> | void
|
|
|
22
35
|
// biome-ignore lint/suspicious/noExplicitAny: required — TypeScript contravariance makes it impossible to type "any constructor" without `any`
|
|
23
36
|
export type ControllerAction = [target: new (...args: any[]) => any, method: string]
|
|
24
37
|
|
|
25
|
-
/**
|
|
26
|
-
export type
|
|
38
|
+
/** Any controller class (derived from {@link ControllerAction} — no fresh `any`). */
|
|
39
|
+
export type ControllerConstructor = ControllerAction[0]
|
|
40
|
+
|
|
41
|
+
/** A lazy controller import: `() => import('#controllers/users_controller')`. */
|
|
42
|
+
export type LazyControllerImport = () => Promise<{ default: ControllerConstructor }>
|
|
43
|
+
|
|
44
|
+
/** Lazy controller tuple: [() => import(...), 'methodName']. */
|
|
45
|
+
export type LazyControllerAction = [loader: LazyControllerImport, method: string]
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A route handler is a closure, a controller tuple, a lazy-import tuple, or a
|
|
49
|
+
* magic string reference `'ControllerName.method'` resolved through the
|
|
50
|
+
* router's controller registry ({@link Router.controllers}).
|
|
51
|
+
*/
|
|
52
|
+
export type RouteHandler = RouteHandlerFunction | ControllerAction | LazyControllerAction | string
|
|
27
53
|
|
|
28
54
|
/** Param matcher — regex or predefined matcher. */
|
|
29
55
|
export type ParamMatcher = RegExp | { pattern: RegExp }
|
|
@@ -45,6 +71,77 @@ export interface RouteDefinition {
|
|
|
45
71
|
domain?: string
|
|
46
72
|
matchers: Record<string, ParamMatcher>
|
|
47
73
|
deprecates?: { version: string; sunset?: string }
|
|
74
|
+
/**
|
|
75
|
+
* Soft-delete flag (AdonisJS `route.markAsDeleted()`). A resource's
|
|
76
|
+
* `.only()`/`.except()`/`.apiOnly()` mark filtered-out routes deleted rather
|
|
77
|
+
* than splicing them, so the builder chain stays intact. Deleted routes are
|
|
78
|
+
* skipped by the match index and `getRoutes()`.
|
|
79
|
+
*/
|
|
80
|
+
deleted?: boolean
|
|
81
|
+
/**
|
|
82
|
+
* Lazily-imported controller (from a `[() => import(...), 'method']` tuple or
|
|
83
|
+
* a resolved string reference). The HttpKernel awaits the loader on first
|
|
84
|
+
* request and promotes it to {@link controller}, so @Guard metadata + DI
|
|
85
|
+
* resolution flow through the normal controller path.
|
|
86
|
+
*/
|
|
87
|
+
lazyController?: { loader: LazyControllerImport; method: string }
|
|
88
|
+
/**
|
|
89
|
+
* Unresolved `'ControllerName.method'` string reference. Resolved to
|
|
90
|
+
* {@link lazyController} against the router's controller registry when the
|
|
91
|
+
* route index is (re)built — a reference to an unregistered controller is a
|
|
92
|
+
* hard error there.
|
|
93
|
+
*/
|
|
94
|
+
stringRef?: { controller: string; method: string }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Distinguish a lazy-import loader from a controller class in a handler tuple.
|
|
99
|
+
* Arrow/plain function loaders (`() => import(...)`) have no own `prototype`
|
|
100
|
+
* property; class constructors do.
|
|
101
|
+
*/
|
|
102
|
+
function isLazyControllerLoader(
|
|
103
|
+
first: ControllerConstructor | LazyControllerImport,
|
|
104
|
+
): first is LazyControllerImport {
|
|
105
|
+
return !('prototype' in first)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Build a route definition from a method, path, and handler. */
|
|
109
|
+
function makeDefinition(method: string, path: string, handler: RouteHandler): RouteDefinition {
|
|
110
|
+
const def: RouteDefinition = {
|
|
111
|
+
method: method.toUpperCase(),
|
|
112
|
+
path,
|
|
113
|
+
handler: null,
|
|
114
|
+
middleware: [],
|
|
115
|
+
inlineMiddleware: [],
|
|
116
|
+
guards: [],
|
|
117
|
+
roles: [],
|
|
118
|
+
permissions: [],
|
|
119
|
+
validators: [],
|
|
120
|
+
matchers: {},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof handler === 'string') {
|
|
124
|
+
// Magic string reference 'ControllerName.method' — split on the LAST dot so
|
|
125
|
+
// controller names may themselves be dotted; resolved at #buildIndex time.
|
|
126
|
+
const dot = handler.lastIndexOf('.')
|
|
127
|
+
if (dot <= 0 || dot === handler.length - 1) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Invalid controller reference "${handler}" — expected the form "ControllerName.method".`,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
def.stringRef = { controller: handler.slice(0, dot), method: handler.slice(dot + 1) }
|
|
133
|
+
} else if (Array.isArray(handler)) {
|
|
134
|
+
const [first, methodName] = handler
|
|
135
|
+
if (isLazyControllerLoader(first)) {
|
|
136
|
+
def.lazyController = { loader: first, method: methodName }
|
|
137
|
+
} else {
|
|
138
|
+
def.controller = { target: first, method: methodName }
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
def.handler = handler
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return def
|
|
48
145
|
}
|
|
49
146
|
|
|
50
147
|
export interface MatchResult {
|
|
@@ -69,56 +166,102 @@ export const matchers = {
|
|
|
69
166
|
},
|
|
70
167
|
}
|
|
71
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Normalise a param matcher into the internal `{ pattern }` shape.
|
|
171
|
+
*
|
|
172
|
+
* A string matcher (AdonisJS `.where('id', '[0-9]+')`) is compiled to an
|
|
173
|
+
* anchored RegExp. AdonisJS builds `new RegExp(matcher)` unanchored and relies
|
|
174
|
+
* on `matchit`'s per-segment anchoring; ream tests matchers per-segment without
|
|
175
|
+
* that implicit anchoring (and its predefined matchers are already `^…$`), so
|
|
176
|
+
* anchoring here reproduces AdonisJS's *effective* full-segment semantics.
|
|
177
|
+
*/
|
|
178
|
+
function compileMatcher(matcher: ParamMatcher | RegExp | string): ParamMatcher {
|
|
179
|
+
if (typeof matcher === 'string') return { pattern: new RegExp(`^(?:${matcher})$`) }
|
|
180
|
+
return matcher instanceof RegExp ? { pattern: matcher } : matcher
|
|
181
|
+
}
|
|
182
|
+
|
|
72
183
|
// ─── RouteBuilder ───────────────────────────────────────────
|
|
73
184
|
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Fluent route builder — mutates the already-registered route definition(s).
|
|
187
|
+
* Backed by an array so a multi-verb `router.route(['GET','POST'], …)` returns
|
|
188
|
+
* ONE builder that fans every mutation across all its verbs; the single-verb
|
|
189
|
+
* helpers (`get`/`post`/…) back it with a one-element array.
|
|
190
|
+
*/
|
|
191
|
+
export class RouteBuilder extends Macroable {
|
|
192
|
+
#routes: RouteDefinition[]
|
|
193
|
+
|
|
194
|
+
constructor(routes: RouteDefinition | RouteDefinition[]) {
|
|
195
|
+
super()
|
|
196
|
+
this.#routes = Array.isArray(routes) ? routes : [routes]
|
|
197
|
+
}
|
|
77
198
|
|
|
78
|
-
|
|
79
|
-
|
|
199
|
+
/** Apply `fn` to every backing definition, returning `this` for chaining. */
|
|
200
|
+
#each(fn: (route: RouteDefinition) => void): this {
|
|
201
|
+
for (const route of this.#routes) fn(route)
|
|
202
|
+
return this
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The primary definition — accessors read from it (all verbs share pattern/name). */
|
|
206
|
+
#primary(): RouteDefinition {
|
|
207
|
+
return this.#routes[0]
|
|
80
208
|
}
|
|
81
209
|
|
|
82
210
|
/** Name this route (for URL generation and redirects). */
|
|
83
211
|
as(name: string): this {
|
|
84
|
-
this.#route
|
|
85
|
-
|
|
212
|
+
return this.#each((route) => {
|
|
213
|
+
route.name = name
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Prepend a URL prefix to this single route (AdonisJS route-level `prefix`). */
|
|
218
|
+
prefix(prefix: string): this {
|
|
219
|
+
return this.#each((route) => {
|
|
220
|
+
route.path = prefix + route.path
|
|
221
|
+
})
|
|
86
222
|
}
|
|
87
223
|
|
|
88
224
|
/** Add named middleware. */
|
|
89
225
|
middleware(...names: string[]): this {
|
|
90
|
-
this.#route
|
|
91
|
-
|
|
226
|
+
return this.#each((route) => {
|
|
227
|
+
route.middleware.push(...names)
|
|
228
|
+
})
|
|
92
229
|
}
|
|
93
230
|
|
|
94
231
|
/** Add inline middleware functions. */
|
|
95
232
|
use(...mw: MiddlewareFunction[]): this {
|
|
96
|
-
this.#route
|
|
97
|
-
|
|
233
|
+
return this.#each((route) => {
|
|
234
|
+
route.inlineMiddleware.push(...mw)
|
|
235
|
+
})
|
|
98
236
|
}
|
|
99
237
|
|
|
100
238
|
/** Add authentication guards. */
|
|
101
239
|
guard(...guards: string[]): this {
|
|
102
|
-
this.#route
|
|
103
|
-
|
|
240
|
+
return this.#each((route) => {
|
|
241
|
+
route.guards.push(...guards)
|
|
242
|
+
})
|
|
104
243
|
}
|
|
105
244
|
|
|
106
245
|
/** Require specific roles. */
|
|
107
246
|
role(...roles: string[]): this {
|
|
108
|
-
this.#route
|
|
109
|
-
|
|
247
|
+
return this.#each((route) => {
|
|
248
|
+
route.roles.push(...roles)
|
|
249
|
+
})
|
|
110
250
|
}
|
|
111
251
|
|
|
112
252
|
/** Require specific permissions. */
|
|
113
253
|
permission(...permissions: string[]): this {
|
|
114
|
-
this.#route
|
|
115
|
-
|
|
254
|
+
return this.#each((route) => {
|
|
255
|
+
route.permissions.push(...permissions)
|
|
256
|
+
})
|
|
116
257
|
}
|
|
117
258
|
|
|
118
|
-
/** Add a param constraint. */
|
|
119
|
-
where(param: string, matcher: ParamMatcher | RegExp): this {
|
|
120
|
-
|
|
121
|
-
return this
|
|
259
|
+
/** Add a param constraint — RegExp, predefined matcher, or a string pattern. */
|
|
260
|
+
where(param: string, matcher: ParamMatcher | RegExp | string): this {
|
|
261
|
+
const compiled = compileMatcher(matcher)
|
|
262
|
+
return this.#each((route) => {
|
|
263
|
+
route.matchers[param] = compiled
|
|
264
|
+
})
|
|
122
265
|
}
|
|
123
266
|
|
|
124
267
|
/**
|
|
@@ -137,41 +280,75 @@ export class RouteBuilder {
|
|
|
137
280
|
* An unregistered name is a hard error — validation is never silently skipped.
|
|
138
281
|
*/
|
|
139
282
|
validate(validator: string): this {
|
|
140
|
-
this.#route
|
|
141
|
-
|
|
283
|
+
return this.#each((route) => {
|
|
284
|
+
route.validators.push(validator)
|
|
285
|
+
})
|
|
142
286
|
}
|
|
143
287
|
|
|
144
288
|
/** Set API version. */
|
|
145
289
|
version(v: string): this {
|
|
146
|
-
this.#route
|
|
147
|
-
|
|
290
|
+
return this.#each((route) => {
|
|
291
|
+
route.version = v
|
|
292
|
+
})
|
|
148
293
|
}
|
|
149
294
|
|
|
150
295
|
/** Restrict to a specific domain. */
|
|
151
296
|
domain(d: string): this {
|
|
152
|
-
this.#route
|
|
153
|
-
|
|
297
|
+
return this.#each((route) => {
|
|
298
|
+
route.domain = d
|
|
299
|
+
})
|
|
154
300
|
}
|
|
155
301
|
|
|
156
302
|
/** Mark as deprecated. */
|
|
157
303
|
deprecates(version: string, options?: { sunset?: string }): this {
|
|
158
|
-
this.#route
|
|
159
|
-
|
|
304
|
+
return this.#each((route) => {
|
|
305
|
+
route.deprecates = { version, sunset: options?.sunset }
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** The route's name, or undefined when unnamed (AdonisJS `route.getName`). */
|
|
310
|
+
getName(): string | undefined {
|
|
311
|
+
return this.#primary().name
|
|
160
312
|
}
|
|
161
313
|
|
|
162
|
-
/**
|
|
314
|
+
/** The route's URL pattern (AdonisJS `route.getPattern`). */
|
|
315
|
+
getPattern(): string {
|
|
316
|
+
return this.#primary().path
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Replace the route's URL pattern (AdonisJS `route.setPattern`). */
|
|
320
|
+
setPattern(pattern: string): this {
|
|
321
|
+
return this.#each((route) => {
|
|
322
|
+
route.path = pattern
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Soft-delete this route so it stops matching (AdonisJS `route.markAsDeleted`). */
|
|
327
|
+
markAsDeleted(): this {
|
|
328
|
+
return this.#each((route) => {
|
|
329
|
+
route.deleted = true
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** True when the route has been soft-deleted (AdonisJS `route.isDeleted`). */
|
|
334
|
+
isDeleted(): boolean {
|
|
335
|
+
return this.#primary().deleted === true
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** @internal Get the underlying (primary) definition. */
|
|
163
339
|
getDefinition(): RouteDefinition {
|
|
164
|
-
return this.#
|
|
340
|
+
return this.#primary()
|
|
165
341
|
}
|
|
166
342
|
}
|
|
167
343
|
|
|
168
344
|
// ─── GroupBuilder ───────────────────────────────────────────
|
|
169
345
|
|
|
170
346
|
/** Fluent group builder — returned by router.group(callback). */
|
|
171
|
-
export class GroupBuilder {
|
|
347
|
+
export class GroupBuilder extends Macroable {
|
|
172
348
|
#routes: RouteDefinition[]
|
|
173
349
|
|
|
174
350
|
constructor(routes: RouteDefinition[]) {
|
|
351
|
+
super()
|
|
175
352
|
this.#routes = routes
|
|
176
353
|
}
|
|
177
354
|
|
|
@@ -207,12 +384,19 @@ export class GroupBuilder {
|
|
|
207
384
|
return this
|
|
208
385
|
}
|
|
209
386
|
|
|
210
|
-
/**
|
|
387
|
+
/**
|
|
388
|
+
* Prefix route names for all routes in the group (AdonisJS `group.as`).
|
|
389
|
+
* Every route MUST already be named — prefixing an unnamed route is
|
|
390
|
+
* meaningless, so it throws rather than silently skipping (AdonisJS parity).
|
|
391
|
+
*/
|
|
211
392
|
as(namePrefix: string): this {
|
|
212
393
|
for (const route of this.#routes) {
|
|
213
|
-
if (route.name) {
|
|
214
|
-
|
|
394
|
+
if (!route.name) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`[E_MISSING_ROUTE_NAME] Cannot apply group name "${namePrefix}": route ${route.method} ${route.path} has no name. Name every route in a named group (\`.as(...)\`), or move the unnamed route out of the group.`,
|
|
397
|
+
)
|
|
215
398
|
}
|
|
399
|
+
route.name = `${namePrefix}.${route.name}`
|
|
216
400
|
}
|
|
217
401
|
return this
|
|
218
402
|
}
|
|
@@ -226,8 +410,8 @@ export class GroupBuilder {
|
|
|
226
410
|
}
|
|
227
411
|
|
|
228
412
|
/** Add param matcher to all routes in the group. */
|
|
229
|
-
where(param: string, matcher: ParamMatcher | RegExp): this {
|
|
230
|
-
const m = matcher
|
|
413
|
+
where(param: string, matcher: ParamMatcher | RegExp | string): this {
|
|
414
|
+
const m = compileMatcher(matcher)
|
|
231
415
|
for (const route of this.#routes) {
|
|
232
416
|
if (!(param in route.matchers)) {
|
|
233
417
|
route.matchers[param] = m
|
|
@@ -237,6 +421,206 @@ export class GroupBuilder {
|
|
|
237
421
|
}
|
|
238
422
|
}
|
|
239
423
|
|
|
424
|
+
// ─── RouteResource ──────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
/** Resourceful action names (AdonisJS resource). */
|
|
427
|
+
export type ResourceAction = 'index' | 'create' | 'store' | 'show' | 'edit' | 'update' | 'destroy'
|
|
428
|
+
|
|
429
|
+
/** Middleware accepted per-action on a resource: a named string or an inline fn. */
|
|
430
|
+
type ResourceMiddleware = string | MiddlewareFunction
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Resourceful route builder — returned by `router.resource()` /
|
|
434
|
+
* `router.shallowResource()` (AdonisJS `RouteResource` parity).
|
|
435
|
+
*
|
|
436
|
+
* Generates the seven RESTful routes (index, create, store, show, edit,
|
|
437
|
+
* update, destroy — `update` registered for both PUT and PATCH) and exposes
|
|
438
|
+
* the fluent filtering/config API: `only`/`except`/`apiOnly`/`params`/`tap`/
|
|
439
|
+
* `where`/`use`/`middleware`/`as`. Nested resources use dot-notation
|
|
440
|
+
* (`posts.comments` → `/posts/:post_id/comments/:id`).
|
|
441
|
+
*/
|
|
442
|
+
export class RouteResource extends Macroable {
|
|
443
|
+
#resource: string
|
|
444
|
+
#controller: ControllerAction[0]
|
|
445
|
+
#shallow: boolean
|
|
446
|
+
#register: (def: RouteDefinition) => void
|
|
447
|
+
#markDirty: () => void
|
|
448
|
+
/** Resource name → param placeholder (`comments` → `:id`, `posts` → `:post_id`). */
|
|
449
|
+
#params: Record<string, string> = {}
|
|
450
|
+
#baseName: string
|
|
451
|
+
|
|
452
|
+
/** The generated routes, in declaration order (AdonisJS `resource.routes`). */
|
|
453
|
+
readonly routes: RouteBuilder[] = []
|
|
454
|
+
|
|
455
|
+
constructor(options: {
|
|
456
|
+
resource: string
|
|
457
|
+
controller: ControllerAction[0]
|
|
458
|
+
shallow: boolean
|
|
459
|
+
register: (def: RouteDefinition) => void
|
|
460
|
+
markDirty: () => void
|
|
461
|
+
}) {
|
|
462
|
+
super()
|
|
463
|
+
const resource = options.resource.replace(/^\//, '').replace(/\/$/, '')
|
|
464
|
+
if (!resource) throw new Error(`Invalid resource name "${options.resource}"`)
|
|
465
|
+
this.#resource = resource
|
|
466
|
+
this.#controller = options.controller
|
|
467
|
+
this.#shallow = options.shallow
|
|
468
|
+
this.#register = options.register
|
|
469
|
+
this.#markDirty = options.markDirty
|
|
470
|
+
this.#baseName = resource.split('.').map(snakeCase).join('.')
|
|
471
|
+
this.#buildRoutes()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
#createRoute(pattern: string, method: string, action: ResourceAction): void {
|
|
475
|
+
const def = makeDefinition(method, `/${pattern}`, [this.#controller, action])
|
|
476
|
+
def.name = `${this.#baseName}.${action}`
|
|
477
|
+
this.#register(def)
|
|
478
|
+
this.routes.push(new RouteBuilder(def))
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
#buildRoutes(): void {
|
|
482
|
+
const segments = this.#resource.split('.')
|
|
483
|
+
const main = segments.pop() ?? this.#resource
|
|
484
|
+
this.#params[main] = ':id'
|
|
485
|
+
|
|
486
|
+
const parentPath = segments
|
|
487
|
+
.map((parent) => {
|
|
488
|
+
const param = `:${snakeCase(singular(parent))}_id`
|
|
489
|
+
this.#params[parent] = param
|
|
490
|
+
return `${parent}/${param}`
|
|
491
|
+
})
|
|
492
|
+
.join('/')
|
|
493
|
+
|
|
494
|
+
const baseURI = parentPath ? `${parentPath}/${main}` : main
|
|
495
|
+
// Shallow member routes drop the parent prefix — the id alone identifies
|
|
496
|
+
// the record (AdonisJS shallow resources).
|
|
497
|
+
const memberBase = this.#shallow ? main : baseURI
|
|
498
|
+
|
|
499
|
+
this.#createRoute(baseURI, 'GET', 'index')
|
|
500
|
+
this.#createRoute(`${baseURI}/create`, 'GET', 'create')
|
|
501
|
+
this.#createRoute(baseURI, 'POST', 'store')
|
|
502
|
+
this.#createRoute(`${memberBase}/:id`, 'GET', 'show')
|
|
503
|
+
this.#createRoute(`${memberBase}/:id/edit`, 'GET', 'edit')
|
|
504
|
+
this.#createRoute(`${memberBase}/:id`, 'PUT', 'update')
|
|
505
|
+
this.#createRoute(`${memberBase}/:id`, 'PATCH', 'update')
|
|
506
|
+
this.#createRoute(`${memberBase}/:id`, 'DELETE', 'destroy')
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Routes whose name ends with one of `names` (the action suffix). */
|
|
510
|
+
#matching(names: string[]): RouteBuilder[] {
|
|
511
|
+
return this.routes.filter((route) => {
|
|
512
|
+
const name = route.getName()
|
|
513
|
+
return name !== undefined && names.some((action) => name.endsWith(action))
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Keep only the given actions; soft-delete the rest (AdonisJS `only`). */
|
|
518
|
+
only(names: ResourceAction[]): this {
|
|
519
|
+
const keep = new Set(this.#matching(names))
|
|
520
|
+
for (const route of this.routes) if (!keep.has(route)) route.markAsDeleted()
|
|
521
|
+
this.#markDirty()
|
|
522
|
+
return this
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Register all actions except the given ones (AdonisJS `except`). */
|
|
526
|
+
except(names: ResourceAction[]): this {
|
|
527
|
+
for (const route of this.#matching(names)) route.markAsDeleted()
|
|
528
|
+
this.#markDirty()
|
|
529
|
+
return this
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Drop the form-rendering `create` + `edit` actions (AdonisJS `apiOnly`). */
|
|
533
|
+
apiOnly(): this {
|
|
534
|
+
return this.except(['create', 'edit'])
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Constrain a param across every action route (AdonisJS `resource.where`). */
|
|
538
|
+
where(key: string, matcher: ParamMatcher | RegExp | string): this {
|
|
539
|
+
for (const route of this.routes) route.where(key, matcher)
|
|
540
|
+
return this
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Configure the resource's routes. `tap(cb)` runs `cb` for every non-deleted
|
|
545
|
+
* route; `tap(actions, cb)` only for routes matching `actions` (AdonisJS `tap`).
|
|
546
|
+
*/
|
|
547
|
+
tap(callback: (route: RouteBuilder) => void): this
|
|
548
|
+
tap(actions: ResourceAction | ResourceAction[], callback: (route: RouteBuilder) => void): this
|
|
549
|
+
tap(
|
|
550
|
+
actionsOrCallback: ResourceAction | ResourceAction[] | ((route: RouteBuilder) => void),
|
|
551
|
+
callback?: (route: RouteBuilder) => void,
|
|
552
|
+
): this {
|
|
553
|
+
if (typeof actionsOrCallback === 'function') {
|
|
554
|
+
for (const route of this.routes) if (!route.isDeleted()) actionsOrCallback(route)
|
|
555
|
+
return this
|
|
556
|
+
}
|
|
557
|
+
const names = Array.isArray(actionsOrCallback) ? actionsOrCallback : [actionsOrCallback]
|
|
558
|
+
if (callback) {
|
|
559
|
+
for (const route of this.#matching(names)) if (!route.isDeleted()) callback(route)
|
|
560
|
+
}
|
|
561
|
+
return this
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Rename the `:id` param of one or more resources (AdonisJS `params`). */
|
|
565
|
+
params(resources: Record<string, string>): this {
|
|
566
|
+
for (const [resource, param] of Object.entries(resources)) {
|
|
567
|
+
const existing = this.#params[resource]
|
|
568
|
+
if (!existing) continue
|
|
569
|
+
this.#params[resource] = `:${param}`
|
|
570
|
+
for (const route of this.routes) {
|
|
571
|
+
route.setPattern(
|
|
572
|
+
route.getPattern().replace(`${resource}/${existing}`, `${resource}/:${param}`),
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
this.#markDirty()
|
|
577
|
+
return this
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Attach middleware to specific actions (or `'*'` for all) — AdonisJS `use`.
|
|
582
|
+
* Named middleware are strings; inline middleware are functions.
|
|
583
|
+
*/
|
|
584
|
+
use(
|
|
585
|
+
actions: ResourceAction | ResourceAction[] | '*',
|
|
586
|
+
middleware: ResourceMiddleware | ResourceMiddleware[],
|
|
587
|
+
): this {
|
|
588
|
+
const list = Array.isArray(middleware) ? middleware : [middleware]
|
|
589
|
+
const apply = (route: RouteBuilder): void => {
|
|
590
|
+
for (const mw of list) {
|
|
591
|
+
if (typeof mw === 'string') route.middleware(mw)
|
|
592
|
+
else route.use(mw)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (actions === '*') this.tap(apply)
|
|
596
|
+
else this.tap(actions, apply)
|
|
597
|
+
return this
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** Alias for {@link RouteResource.use} (AdonisJS `middleware`). */
|
|
601
|
+
middleware(
|
|
602
|
+
actions: ResourceAction | ResourceAction[] | '*',
|
|
603
|
+
middleware: ResourceMiddleware | ResourceMiddleware[],
|
|
604
|
+
): this {
|
|
605
|
+
return this.use(actions, middleware)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Rename the base of every route name (AdonisJS `as`). `normalizeName`
|
|
610
|
+
* snake_cases the new base by default.
|
|
611
|
+
*/
|
|
612
|
+
as(name: string, normalizeName = true): this {
|
|
613
|
+
const newBase = normalizeName ? snakeCase(name) : name
|
|
614
|
+
for (const route of this.routes) {
|
|
615
|
+
const current = route.getName()
|
|
616
|
+
if (current) route.as(current.replace(this.#baseName, newBase))
|
|
617
|
+
}
|
|
618
|
+
this.#baseName = newBase
|
|
619
|
+
this.#markDirty()
|
|
620
|
+
return this
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
240
624
|
// ─── View engine helper ───────────────────────────────────────
|
|
241
625
|
|
|
242
626
|
interface ViewEngine {
|
|
@@ -254,7 +638,7 @@ function isViewEngine(value: unknown): value is ViewEngine {
|
|
|
254
638
|
|
|
255
639
|
// ─── OnRouteBuilder ─────────────────────────────────────────
|
|
256
640
|
|
|
257
|
-
/** Builder for on(path).render(view) and on(path).redirect(
|
|
641
|
+
/** Builder for on(path).render(view) and on(path).redirect(namedRoute). */
|
|
258
642
|
export class OnRouteBuilder {
|
|
259
643
|
#router: Router
|
|
260
644
|
#path: string
|
|
@@ -277,17 +661,21 @@ export class OnRouteBuilder {
|
|
|
277
661
|
})
|
|
278
662
|
}
|
|
279
663
|
|
|
280
|
-
/**
|
|
281
|
-
|
|
664
|
+
/**
|
|
665
|
+
* Redirect to a NAMED route (AdonisJS brisk `redirect`). Mirrors AdonisJS:
|
|
666
|
+
* the first argument is a route name, not a path — use {@link redirectToPath}
|
|
667
|
+
* for a fixed URL.
|
|
668
|
+
*/
|
|
669
|
+
redirect(name: string, params?: Record<string, string>, status = 302): RouteBuilder {
|
|
282
670
|
return this.#router.get(this.#path, async (ctx) => {
|
|
283
|
-
ctx.response.redirect().status(status).
|
|
671
|
+
ctx.response.redirect().status(status).toRoute(name, params)
|
|
284
672
|
})
|
|
285
673
|
}
|
|
286
674
|
|
|
287
|
-
/** Redirect to a
|
|
288
|
-
|
|
675
|
+
/** Redirect to a fixed path/URL (AdonisJS brisk `redirectToPath`). */
|
|
676
|
+
redirectToPath(target: string, status = 302): RouteBuilder {
|
|
289
677
|
return this.#router.get(this.#path, async (ctx) => {
|
|
290
|
-
ctx.response.redirect().status(status).
|
|
678
|
+
ctx.response.redirect().status(status).toPath(target)
|
|
291
679
|
})
|
|
292
680
|
}
|
|
293
681
|
}
|
|
@@ -295,11 +683,15 @@ export class OnRouteBuilder {
|
|
|
295
683
|
// ─── Router ─────────────────────────────────────────────────
|
|
296
684
|
|
|
297
685
|
/** Main Router. */
|
|
298
|
-
export class Router {
|
|
686
|
+
export class Router extends Macroable {
|
|
299
687
|
#routes: RouteDefinition[] = []
|
|
300
688
|
#globalMatchers: Record<string, ParamMatcher> = {}
|
|
301
689
|
#routerMiddleware: MiddlewareFunction[] = []
|
|
302
690
|
#namedMiddleware: Map<string, MiddlewareFunction> = new Map()
|
|
691
|
+
/** Controller registry: name → lazy import, for string handler references. */
|
|
692
|
+
#controllers: Map<string, LazyControllerImport> = new Map()
|
|
693
|
+
/** APP_KEY-backed signed-URL helper (injected by the Ignitor when set). */
|
|
694
|
+
#signedUrl?: SignedUrl
|
|
303
695
|
|
|
304
696
|
/** Index: static routes by "METHOD:path" for O(1) exact match. */
|
|
305
697
|
#staticIndex: Map<string, RouteDefinition> = new Map()
|
|
@@ -327,19 +719,49 @@ export class Router {
|
|
|
327
719
|
}
|
|
328
720
|
|
|
329
721
|
/**
|
|
330
|
-
* Register named middleware collection
|
|
722
|
+
* Register a named middleware collection and return per-name FACTORIES
|
|
723
|
+
* (AdonisJS `defineNamedMiddleware`). Call a factory to bind per-route
|
|
724
|
+
* arguments, then attach the result inline:
|
|
725
|
+
*
|
|
331
726
|
* export const middleware = router.named({
|
|
332
727
|
* auth: () => import('#middleware/auth_middleware'),
|
|
333
728
|
* })
|
|
729
|
+
* router.get('/admin', [AdminController, 'index']).use([middleware.auth({ guards: ['web'] })])
|
|
730
|
+
*
|
|
731
|
+
* By-name usage (`route.middleware('auth')`) still runs the middleware with
|
|
732
|
+
* no arguments — the factory form is only needed to pass `args` to `handle`.
|
|
334
733
|
*/
|
|
335
|
-
named(
|
|
336
|
-
|
|
734
|
+
named(
|
|
735
|
+
collection: Record<string, MiddlewareEntry>,
|
|
736
|
+
): Record<string, (args?: unknown) => MiddlewareFunction> {
|
|
737
|
+
const factories: Record<string, (args?: unknown) => MiddlewareFunction> = {}
|
|
337
738
|
for (const [name, mw] of Object.entries(collection)) {
|
|
338
|
-
|
|
339
|
-
this.#namedMiddleware.set(name,
|
|
340
|
-
|
|
739
|
+
// By-name resolution (`route.middleware('auth')`) uses the no-arg form.
|
|
740
|
+
this.#namedMiddleware.set(name, resolveMiddlewareEntry(mw))
|
|
741
|
+
// The returned factory bakes in per-route args for inline `.use()`.
|
|
742
|
+
factories[name] = resolveParametrizedMiddlewareEntry(mw)
|
|
743
|
+
}
|
|
744
|
+
return factories
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Register controllers for magic string handler references (AdonisJS lazy
|
|
749
|
+
* controller pattern). Each entry maps a controller name to a lazy import:
|
|
750
|
+
*
|
|
751
|
+
* router.controllers({ UsersController: () => import('#controllers/users_controller') })
|
|
752
|
+
* router.get('/users', 'UsersController.index')
|
|
753
|
+
*
|
|
754
|
+
* The class is imported on first request and resolved through the IoC
|
|
755
|
+
* container (so @inject + @Guard/@Role decorators work). A string reference
|
|
756
|
+
* to a name that was never registered fails loudly when the route index is
|
|
757
|
+
* built.
|
|
758
|
+
*/
|
|
759
|
+
controllers(map: Record<string, LazyControllerImport>): this {
|
|
760
|
+
for (const [name, loader] of Object.entries(map)) {
|
|
761
|
+
this.#controllers.set(name, loader)
|
|
341
762
|
}
|
|
342
|
-
|
|
763
|
+
this.#indexDirty = true
|
|
764
|
+
return this
|
|
343
765
|
}
|
|
344
766
|
|
|
345
767
|
/** Get the router-level middleware stack. */
|
|
@@ -354,12 +776,20 @@ export class Router {
|
|
|
354
776
|
|
|
355
777
|
// ─── Route registration ───────────────────────────────────
|
|
356
778
|
|
|
357
|
-
/**
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
779
|
+
/**
|
|
780
|
+
* Register a route for one verb or several (AdonisJS `route(pattern, methods)`
|
|
781
|
+
* accepts an array). A multi-verb call returns a single builder that
|
|
782
|
+
* configures every verb at once.
|
|
783
|
+
*/
|
|
784
|
+
route(method: string | string[], path: string, handler: RouteHandler): RouteBuilder {
|
|
785
|
+
const methods = Array.isArray(method) ? method : [method]
|
|
786
|
+
const defs = methods.map((verb) => {
|
|
787
|
+
const def = this.#createDefinition(verb, path, handler)
|
|
788
|
+
this.#routes.push(def)
|
|
789
|
+
return def
|
|
790
|
+
})
|
|
361
791
|
this.#indexDirty = true
|
|
362
|
-
return new RouteBuilder(
|
|
792
|
+
return new RouteBuilder(defs)
|
|
363
793
|
}
|
|
364
794
|
|
|
365
795
|
get(path: string, handler: RouteHandler): RouteBuilder {
|
|
@@ -398,40 +828,54 @@ export class Router {
|
|
|
398
828
|
// ─── Resource routes ──────────────────────────────────────
|
|
399
829
|
|
|
400
830
|
/**
|
|
401
|
-
* Register resourceful routes for a controller.
|
|
402
|
-
*
|
|
831
|
+
* Register resourceful routes for a controller (AdonisJS `resource`).
|
|
832
|
+
*
|
|
833
|
+
* Generates index / create / store / show / edit / update / destroy —
|
|
834
|
+
* `update` for both PUT and PATCH. Returns a {@link RouteResource} for
|
|
835
|
+
* fluent filtering: `.only()`, `.except()`, `.apiOnly()`, `.params()`,
|
|
836
|
+
* `.tap()`, `.where()`, `.use()`, `.as()`.
|
|
837
|
+
*
|
|
838
|
+
* Nested resources use dot-notation (`posts.comments` →
|
|
839
|
+
* `/posts/:post_id/comments/:id`).
|
|
403
840
|
*
|
|
404
841
|
* Usage:
|
|
405
842
|
* router.resource('posts', PostsController)
|
|
406
|
-
*
|
|
407
|
-
* // POST /posts → PostsController.store
|
|
408
|
-
* // GET /posts/:id → PostsController.show
|
|
409
|
-
* // PUT /posts/:id → PostsController.update
|
|
410
|
-
* // DELETE /posts/:id → PostsController.destroy
|
|
843
|
+
* router.resource('posts', PostsController).apiOnly()
|
|
411
844
|
*/
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
for (const { method, suffix, action, nameSuffix } of actions) {
|
|
427
|
-
const def = this.#createDefinition(method, `/${path}${suffix}`, [controller, action])
|
|
428
|
-
def.name = `${baseName}.${nameSuffix}`
|
|
429
|
-
routes.push(def)
|
|
430
|
-
this.#routes.push(def)
|
|
431
|
-
}
|
|
432
|
-
this.#indexDirty = true
|
|
845
|
+
resource(path: string, controller: ControllerAction[0]): RouteResource {
|
|
846
|
+
return new RouteResource({
|
|
847
|
+
resource: path,
|
|
848
|
+
controller,
|
|
849
|
+
shallow: false,
|
|
850
|
+
register: (def) => {
|
|
851
|
+
this.#routes.push(def)
|
|
852
|
+
this.#indexDirty = true
|
|
853
|
+
},
|
|
854
|
+
markDirty: () => {
|
|
855
|
+
this.#indexDirty = true
|
|
856
|
+
},
|
|
857
|
+
})
|
|
858
|
+
}
|
|
433
859
|
|
|
434
|
-
|
|
860
|
+
/**
|
|
861
|
+
* Register a shallow resource (AdonisJS `shallowResource`). Member routes
|
|
862
|
+
* (show/edit/update/destroy) drop the parent prefix, since the record id
|
|
863
|
+
* alone identifies it: `posts.comments` → `/comments/:id` for members but
|
|
864
|
+
* `/posts/:post_id/comments` for the collection.
|
|
865
|
+
*/
|
|
866
|
+
shallowResource(path: string, controller: ControllerAction[0]): RouteResource {
|
|
867
|
+
return new RouteResource({
|
|
868
|
+
resource: path,
|
|
869
|
+
controller,
|
|
870
|
+
shallow: true,
|
|
871
|
+
register: (def) => {
|
|
872
|
+
this.#routes.push(def)
|
|
873
|
+
this.#indexDirty = true
|
|
874
|
+
},
|
|
875
|
+
markDirty: () => {
|
|
876
|
+
this.#indexDirty = true
|
|
877
|
+
},
|
|
878
|
+
})
|
|
435
879
|
}
|
|
436
880
|
|
|
437
881
|
// ─── Groups ───────────────────────────────────────────────
|
|
@@ -508,8 +952,8 @@ export class Router {
|
|
|
508
952
|
// ─── Global matchers ──────────────────────────────────────
|
|
509
953
|
|
|
510
954
|
/** Set a global param matcher (applied to all routes). */
|
|
511
|
-
where(param: string, matcher: ParamMatcher | RegExp): this {
|
|
512
|
-
this.#globalMatchers[param] = matcher
|
|
955
|
+
where(param: string, matcher: ParamMatcher | RegExp | string): this {
|
|
956
|
+
this.#globalMatchers[param] = compileMatcher(matcher)
|
|
513
957
|
return this
|
|
514
958
|
}
|
|
515
959
|
|
|
@@ -522,6 +966,20 @@ export class Router {
|
|
|
522
966
|
this.#nameIndex.clear()
|
|
523
967
|
|
|
524
968
|
for (const route of this.#routes) {
|
|
969
|
+
// Soft-deleted routes (resource .only()/.except()/.apiOnly()) never match.
|
|
970
|
+
if (route.deleted) continue
|
|
971
|
+
// Resolve a string handler reference against the controller registry.
|
|
972
|
+
if (route.stringRef && !route.lazyController) {
|
|
973
|
+
const loader = this.#controllers.get(route.stringRef.controller)
|
|
974
|
+
if (!loader) {
|
|
975
|
+
throw new Error(
|
|
976
|
+
`[E_UNREGISTERED_CONTROLLER] Route ${route.method} ${route.path} references ` +
|
|
977
|
+
`controller '${route.stringRef.controller}', which is not registered. Register it ` +
|
|
978
|
+
`with router.controllers({ ${route.stringRef.controller}: () => import('...') }).`,
|
|
979
|
+
)
|
|
980
|
+
}
|
|
981
|
+
route.lazyController = { loader, method: route.stringRef.method }
|
|
982
|
+
}
|
|
525
983
|
// Named route index
|
|
526
984
|
if (route.name) {
|
|
527
985
|
this.#nameIndex.set(route.name, route)
|
|
@@ -647,6 +1105,33 @@ export class Router {
|
|
|
647
1105
|
return this.urlFor(name, params)
|
|
648
1106
|
}
|
|
649
1107
|
|
|
1108
|
+
/** @internal Inject the APP_KEY-backed signed-URL helper (wired by the Ignitor). */
|
|
1109
|
+
setSignedUrl(signedUrl: SignedUrl): void {
|
|
1110
|
+
this.#signedUrl = signedUrl
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Generate a tamper-proof signed URL for a named route (AdonisJS
|
|
1115
|
+
* `makeSignedUrl`). Delegates to the {@link SignedUrl} helper: appends an HMAC
|
|
1116
|
+
* `signature`, plus a signed `expires` timestamp when `expiresIn` is given.
|
|
1117
|
+
* Verified on the request side by `request.hasValidSignature()`.
|
|
1118
|
+
*
|
|
1119
|
+
* Requires an APP_KEY-backed signer (registered when `APP_KEY` is set).
|
|
1120
|
+
*/
|
|
1121
|
+
makeSignedUrl(name: string, params?: Record<string, string>, options?: SignedUrlOptions): string {
|
|
1122
|
+
if (!this.#signedUrl) {
|
|
1123
|
+
throw new Error(
|
|
1124
|
+
'[E_NO_APP_KEY] makeSignedUrl() requires an APP_KEY-backed signer. Set APP_KEY (>= 16 chars) so the encryption service is registered.',
|
|
1125
|
+
)
|
|
1126
|
+
}
|
|
1127
|
+
const path = this.urlFor(name, params)
|
|
1128
|
+
const withQs = options?.qs ? `${path}?${new URLSearchParams(options.qs).toString()}` : path
|
|
1129
|
+
return this.#signedUrl.make(withQs, {
|
|
1130
|
+
expiresIn: options?.expiresIn,
|
|
1131
|
+
purpose: options?.purpose,
|
|
1132
|
+
})
|
|
1133
|
+
}
|
|
1134
|
+
|
|
650
1135
|
/**
|
|
651
1136
|
* Map of every NAMED route's `name` → path pattern, e.g.
|
|
652
1137
|
* `{ 'users.show': '/users/:id' }`. Serialize this into a page so a
|
|
@@ -663,11 +1148,62 @@ export class Router {
|
|
|
663
1148
|
return manifest
|
|
664
1149
|
}
|
|
665
1150
|
|
|
1151
|
+
/**
|
|
1152
|
+
* Generate a TypeScript source string typing every named route and its
|
|
1153
|
+
* params (AdonisJS `generateTypes`) — a `RouteName` union plus a
|
|
1154
|
+
* `RouteParams` map inferred from each pattern's `:param` segments. Write it
|
|
1155
|
+
* to a `.d.ts` so `urlFor(name, params)` is type-checked. Returns the source;
|
|
1156
|
+
* the caller decides where to persist it.
|
|
1157
|
+
*/
|
|
1158
|
+
generateTypes(): string {
|
|
1159
|
+
if (this.#indexDirty) this.#buildIndex()
|
|
1160
|
+
const names = [...this.#nameIndex.keys()].sort()
|
|
1161
|
+
if (names.length === 0) {
|
|
1162
|
+
return 'export type RouteName = never\nexport interface RouteParams {}\n'
|
|
1163
|
+
}
|
|
1164
|
+
const union = names.map((name) => ` | ${JSON.stringify(name)}`).join('\n')
|
|
1165
|
+
const entries = names.map((name) => {
|
|
1166
|
+
const pattern = this.#nameIndex.get(name)?.path ?? ''
|
|
1167
|
+
const params = [...pattern.matchAll(/:([A-Za-z_]\w*)(\??)/g)]
|
|
1168
|
+
const shape =
|
|
1169
|
+
params.length === 0
|
|
1170
|
+
? 'Record<string, never>'
|
|
1171
|
+
: `{ ${params.map((m) => `${JSON.stringify(m[1])}${m[2] === '?' ? '?' : ''}: string`).join('; ')} }`
|
|
1172
|
+
return ` ${JSON.stringify(name)}: ${shape}`
|
|
1173
|
+
})
|
|
1174
|
+
return `export type RouteName =\n${union}\n\nexport interface RouteParams {\n${entries.join('\n')}\n}\n`
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// ─── Lookup ───────────────────────────────────────────────
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Find a registered route by name or exact pattern (AdonisJS `router.find`).
|
|
1181
|
+
* Returns null when nothing matches. Soft-deleted routes are ignored.
|
|
1182
|
+
*/
|
|
1183
|
+
find(identifier: string): RouteDefinition | null {
|
|
1184
|
+
if (this.#indexDirty) this.#buildIndex()
|
|
1185
|
+
const byName = this.#nameIndex.get(identifier)
|
|
1186
|
+
if (byName) return byName
|
|
1187
|
+
return this.#routes.find((route) => !route.deleted && route.path === identifier) ?? null
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/** Like {@link find} but throws when the route is absent (AdonisJS `findOrFail`). */
|
|
1191
|
+
findOrFail(identifier: string): RouteDefinition {
|
|
1192
|
+
const route = this.find(identifier)
|
|
1193
|
+
if (!route) throw new Error(`Cannot find route for '${identifier}'`)
|
|
1194
|
+
return route
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/** True when a route with the given name or pattern exists (AdonisJS `router.has`). */
|
|
1198
|
+
has(identifier: string): boolean {
|
|
1199
|
+
return this.find(identifier) !== null
|
|
1200
|
+
}
|
|
1201
|
+
|
|
666
1202
|
// ─── Accessors ────────────────────────────────────────────
|
|
667
1203
|
|
|
668
|
-
/** Get all registered routes (for OpenAPI generation, introspection). */
|
|
1204
|
+
/** Get all registered (non-deleted) routes (for OpenAPI generation, introspection). */
|
|
669
1205
|
getRoutes(): RouteDefinition[] {
|
|
670
|
-
return
|
|
1206
|
+
return this.#routes.filter((route) => !route.deleted)
|
|
671
1207
|
}
|
|
672
1208
|
|
|
673
1209
|
/**
|
|
@@ -680,6 +1216,7 @@ export class Router {
|
|
|
680
1216
|
this.#routes = []
|
|
681
1217
|
this.#routerMiddleware = []
|
|
682
1218
|
this.#namedMiddleware.clear()
|
|
1219
|
+
this.#controllers.clear()
|
|
683
1220
|
this.#globalMatchers = {}
|
|
684
1221
|
this.#indexDirty = true
|
|
685
1222
|
}
|
|
@@ -691,26 +1228,7 @@ export class Router {
|
|
|
691
1228
|
// ─── Internals ────────────────────────────────────────────
|
|
692
1229
|
|
|
693
1230
|
#createDefinition(method: string, path: string, handler: RouteHandler): RouteDefinition {
|
|
694
|
-
|
|
695
|
-
method: method.toUpperCase(),
|
|
696
|
-
path,
|
|
697
|
-
handler: null,
|
|
698
|
-
middleware: [],
|
|
699
|
-
inlineMiddleware: [],
|
|
700
|
-
guards: [],
|
|
701
|
-
roles: [],
|
|
702
|
-
permissions: [],
|
|
703
|
-
validators: [],
|
|
704
|
-
matchers: {},
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (Array.isArray(handler)) {
|
|
708
|
-
def.controller = { target: handler[0], method: handler[1] }
|
|
709
|
-
} else {
|
|
710
|
-
def.handler = handler
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
return def
|
|
1231
|
+
return makeDefinition(method, path, handler)
|
|
714
1232
|
}
|
|
715
1233
|
|
|
716
1234
|
#validateMatchers(
|