@c9up/ream 0.1.25 → 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 +53 -7
- 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 +404 -16
- 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 +87 -18
- package/src/http/RedirectBuilder.ts +4 -1
- package/src/http/Request.ts +264 -34
- package/src/http/Response.ts +478 -27
- 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/dist/router/Router.js
CHANGED
|
@@ -4,7 +4,54 @@
|
|
|
4
4
|
* @implements FR24, FR25, FR26, FR27, FR28
|
|
5
5
|
*/
|
|
6
6
|
import { safeDecodeURIComponent } from '../http/urlDecode.js';
|
|
7
|
-
import { resolveMiddlewareEntry } from '../server/Server.js';
|
|
7
|
+
import { resolveMiddlewareEntry, resolveParametrizedMiddlewareEntry } from '../server/Server.js';
|
|
8
|
+
import { singular, snakeCase } from '../utils/inflect.js';
|
|
9
|
+
import { Macroable } from '../utils/Macroable.js';
|
|
10
|
+
/**
|
|
11
|
+
* Distinguish a lazy-import loader from a controller class in a handler tuple.
|
|
12
|
+
* Arrow/plain function loaders (`() => import(...)`) have no own `prototype`
|
|
13
|
+
* property; class constructors do.
|
|
14
|
+
*/
|
|
15
|
+
function isLazyControllerLoader(first) {
|
|
16
|
+
return !('prototype' in first);
|
|
17
|
+
}
|
|
18
|
+
/** Build a route definition from a method, path, and handler. */
|
|
19
|
+
function makeDefinition(method, path, handler) {
|
|
20
|
+
const def = {
|
|
21
|
+
method: method.toUpperCase(),
|
|
22
|
+
path,
|
|
23
|
+
handler: null,
|
|
24
|
+
middleware: [],
|
|
25
|
+
inlineMiddleware: [],
|
|
26
|
+
guards: [],
|
|
27
|
+
roles: [],
|
|
28
|
+
permissions: [],
|
|
29
|
+
validators: [],
|
|
30
|
+
matchers: {},
|
|
31
|
+
};
|
|
32
|
+
if (typeof handler === 'string') {
|
|
33
|
+
// Magic string reference 'ControllerName.method' — split on the LAST dot so
|
|
34
|
+
// controller names may themselves be dotted; resolved at #buildIndex time.
|
|
35
|
+
const dot = handler.lastIndexOf('.');
|
|
36
|
+
if (dot <= 0 || dot === handler.length - 1) {
|
|
37
|
+
throw new Error(`Invalid controller reference "${handler}" — expected the form "ControllerName.method".`);
|
|
38
|
+
}
|
|
39
|
+
def.stringRef = { controller: handler.slice(0, dot), method: handler.slice(dot + 1) };
|
|
40
|
+
}
|
|
41
|
+
else if (Array.isArray(handler)) {
|
|
42
|
+
const [first, methodName] = handler;
|
|
43
|
+
if (isLazyControllerLoader(first)) {
|
|
44
|
+
def.lazyController = { loader: first, method: methodName };
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
def.controller = { target: first, method: methodName };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
def.handler = handler;
|
|
52
|
+
}
|
|
53
|
+
return def;
|
|
54
|
+
}
|
|
8
55
|
// ─── Matchers ───────────────────────────────────────────────
|
|
9
56
|
export const matchers = {
|
|
10
57
|
/** Match numeric params only. */
|
|
@@ -20,47 +67,91 @@ export const matchers = {
|
|
|
20
67
|
return { pattern: /^[a-z0-9]+(?:-[a-z0-9]+)*$/ };
|
|
21
68
|
},
|
|
22
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* Normalise a param matcher into the internal `{ pattern }` shape.
|
|
72
|
+
*
|
|
73
|
+
* A string matcher (AdonisJS `.where('id', '[0-9]+')`) is compiled to an
|
|
74
|
+
* anchored RegExp. AdonisJS builds `new RegExp(matcher)` unanchored and relies
|
|
75
|
+
* on `matchit`'s per-segment anchoring; ream tests matchers per-segment without
|
|
76
|
+
* that implicit anchoring (and its predefined matchers are already `^…$`), so
|
|
77
|
+
* anchoring here reproduces AdonisJS's *effective* full-segment semantics.
|
|
78
|
+
*/
|
|
79
|
+
function compileMatcher(matcher) {
|
|
80
|
+
if (typeof matcher === 'string')
|
|
81
|
+
return { pattern: new RegExp(`^(?:${matcher})$`) };
|
|
82
|
+
return matcher instanceof RegExp ? { pattern: matcher } : matcher;
|
|
83
|
+
}
|
|
23
84
|
// ─── RouteBuilder ───────────────────────────────────────────
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Fluent route builder — mutates the already-registered route definition(s).
|
|
87
|
+
* Backed by an array so a multi-verb `router.route(['GET','POST'], …)` returns
|
|
88
|
+
* ONE builder that fans every mutation across all its verbs; the single-verb
|
|
89
|
+
* helpers (`get`/`post`/…) back it with a one-element array.
|
|
90
|
+
*/
|
|
91
|
+
export class RouteBuilder extends Macroable {
|
|
92
|
+
#routes;
|
|
93
|
+
constructor(routes) {
|
|
94
|
+
super();
|
|
95
|
+
this.#routes = Array.isArray(routes) ? routes : [routes];
|
|
96
|
+
}
|
|
97
|
+
/** Apply `fn` to every backing definition, returning `this` for chaining. */
|
|
98
|
+
#each(fn) {
|
|
99
|
+
for (const route of this.#routes)
|
|
100
|
+
fn(route);
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
/** The primary definition — accessors read from it (all verbs share pattern/name). */
|
|
104
|
+
#primary() {
|
|
105
|
+
return this.#routes[0];
|
|
29
106
|
}
|
|
30
107
|
/** Name this route (for URL generation and redirects). */
|
|
31
108
|
as(name) {
|
|
32
|
-
this.#route
|
|
33
|
-
|
|
109
|
+
return this.#each((route) => {
|
|
110
|
+
route.name = name;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Prepend a URL prefix to this single route (AdonisJS route-level `prefix`). */
|
|
114
|
+
prefix(prefix) {
|
|
115
|
+
return this.#each((route) => {
|
|
116
|
+
route.path = prefix + route.path;
|
|
117
|
+
});
|
|
34
118
|
}
|
|
35
119
|
/** Add named middleware. */
|
|
36
120
|
middleware(...names) {
|
|
37
|
-
this.#route
|
|
38
|
-
|
|
121
|
+
return this.#each((route) => {
|
|
122
|
+
route.middleware.push(...names);
|
|
123
|
+
});
|
|
39
124
|
}
|
|
40
125
|
/** Add inline middleware functions. */
|
|
41
126
|
use(...mw) {
|
|
42
|
-
this.#route
|
|
43
|
-
|
|
127
|
+
return this.#each((route) => {
|
|
128
|
+
route.inlineMiddleware.push(...mw);
|
|
129
|
+
});
|
|
44
130
|
}
|
|
45
131
|
/** Add authentication guards. */
|
|
46
132
|
guard(...guards) {
|
|
47
|
-
this.#route
|
|
48
|
-
|
|
133
|
+
return this.#each((route) => {
|
|
134
|
+
route.guards.push(...guards);
|
|
135
|
+
});
|
|
49
136
|
}
|
|
50
137
|
/** Require specific roles. */
|
|
51
138
|
role(...roles) {
|
|
52
|
-
this.#route
|
|
53
|
-
|
|
139
|
+
return this.#each((route) => {
|
|
140
|
+
route.roles.push(...roles);
|
|
141
|
+
});
|
|
54
142
|
}
|
|
55
143
|
/** Require specific permissions. */
|
|
56
144
|
permission(...permissions) {
|
|
57
|
-
this.#route
|
|
58
|
-
|
|
145
|
+
return this.#each((route) => {
|
|
146
|
+
route.permissions.push(...permissions);
|
|
147
|
+
});
|
|
59
148
|
}
|
|
60
|
-
/** Add a param constraint. */
|
|
149
|
+
/** Add a param constraint — RegExp, predefined matcher, or a string pattern. */
|
|
61
150
|
where(param, matcher) {
|
|
62
|
-
|
|
63
|
-
return this
|
|
151
|
+
const compiled = compileMatcher(matcher);
|
|
152
|
+
return this.#each((route) => {
|
|
153
|
+
route.matchers[param] = compiled;
|
|
154
|
+
});
|
|
64
155
|
}
|
|
65
156
|
/**
|
|
66
157
|
* Validate the request body against a named validator. At request time the
|
|
@@ -78,34 +169,63 @@ export class RouteBuilder {
|
|
|
78
169
|
* An unregistered name is a hard error — validation is never silently skipped.
|
|
79
170
|
*/
|
|
80
171
|
validate(validator) {
|
|
81
|
-
this.#route
|
|
82
|
-
|
|
172
|
+
return this.#each((route) => {
|
|
173
|
+
route.validators.push(validator);
|
|
174
|
+
});
|
|
83
175
|
}
|
|
84
176
|
/** Set API version. */
|
|
85
177
|
version(v) {
|
|
86
|
-
this.#route
|
|
87
|
-
|
|
178
|
+
return this.#each((route) => {
|
|
179
|
+
route.version = v;
|
|
180
|
+
});
|
|
88
181
|
}
|
|
89
182
|
/** Restrict to a specific domain. */
|
|
90
183
|
domain(d) {
|
|
91
|
-
this.#route
|
|
92
|
-
|
|
184
|
+
return this.#each((route) => {
|
|
185
|
+
route.domain = d;
|
|
186
|
+
});
|
|
93
187
|
}
|
|
94
188
|
/** Mark as deprecated. */
|
|
95
189
|
deprecates(version, options) {
|
|
96
|
-
this.#route
|
|
97
|
-
|
|
190
|
+
return this.#each((route) => {
|
|
191
|
+
route.deprecates = { version, sunset: options?.sunset };
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/** The route's name, or undefined when unnamed (AdonisJS `route.getName`). */
|
|
195
|
+
getName() {
|
|
196
|
+
return this.#primary().name;
|
|
197
|
+
}
|
|
198
|
+
/** The route's URL pattern (AdonisJS `route.getPattern`). */
|
|
199
|
+
getPattern() {
|
|
200
|
+
return this.#primary().path;
|
|
98
201
|
}
|
|
99
|
-
/**
|
|
202
|
+
/** Replace the route's URL pattern (AdonisJS `route.setPattern`). */
|
|
203
|
+
setPattern(pattern) {
|
|
204
|
+
return this.#each((route) => {
|
|
205
|
+
route.path = pattern;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/** Soft-delete this route so it stops matching (AdonisJS `route.markAsDeleted`). */
|
|
209
|
+
markAsDeleted() {
|
|
210
|
+
return this.#each((route) => {
|
|
211
|
+
route.deleted = true;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/** True when the route has been soft-deleted (AdonisJS `route.isDeleted`). */
|
|
215
|
+
isDeleted() {
|
|
216
|
+
return this.#primary().deleted === true;
|
|
217
|
+
}
|
|
218
|
+
/** @internal Get the underlying (primary) definition. */
|
|
100
219
|
getDefinition() {
|
|
101
|
-
return this.#
|
|
220
|
+
return this.#primary();
|
|
102
221
|
}
|
|
103
222
|
}
|
|
104
223
|
// ─── GroupBuilder ───────────────────────────────────────────
|
|
105
224
|
/** Fluent group builder — returned by router.group(callback). */
|
|
106
|
-
export class GroupBuilder {
|
|
225
|
+
export class GroupBuilder extends Macroable {
|
|
107
226
|
#routes;
|
|
108
227
|
constructor(routes) {
|
|
228
|
+
super();
|
|
109
229
|
this.#routes = routes;
|
|
110
230
|
}
|
|
111
231
|
/** Set URL prefix for all routes in the group. */
|
|
@@ -136,12 +256,17 @@ export class GroupBuilder {
|
|
|
136
256
|
}
|
|
137
257
|
return this;
|
|
138
258
|
}
|
|
139
|
-
/**
|
|
259
|
+
/**
|
|
260
|
+
* Prefix route names for all routes in the group (AdonisJS `group.as`).
|
|
261
|
+
* Every route MUST already be named — prefixing an unnamed route is
|
|
262
|
+
* meaningless, so it throws rather than silently skipping (AdonisJS parity).
|
|
263
|
+
*/
|
|
140
264
|
as(namePrefix) {
|
|
141
265
|
for (const route of this.#routes) {
|
|
142
|
-
if (route.name) {
|
|
143
|
-
|
|
266
|
+
if (!route.name) {
|
|
267
|
+
throw new Error(`[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.`);
|
|
144
268
|
}
|
|
269
|
+
route.name = `${namePrefix}.${route.name}`;
|
|
145
270
|
}
|
|
146
271
|
return this;
|
|
147
272
|
}
|
|
@@ -154,7 +279,7 @@ export class GroupBuilder {
|
|
|
154
279
|
}
|
|
155
280
|
/** Add param matcher to all routes in the group. */
|
|
156
281
|
where(param, matcher) {
|
|
157
|
-
const m = matcher
|
|
282
|
+
const m = compileMatcher(matcher);
|
|
158
283
|
for (const route of this.#routes) {
|
|
159
284
|
if (!(param in route.matchers)) {
|
|
160
285
|
route.matchers[param] = m;
|
|
@@ -163,6 +288,172 @@ export class GroupBuilder {
|
|
|
163
288
|
return this;
|
|
164
289
|
}
|
|
165
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Resourceful route builder — returned by `router.resource()` /
|
|
293
|
+
* `router.shallowResource()` (AdonisJS `RouteResource` parity).
|
|
294
|
+
*
|
|
295
|
+
* Generates the seven RESTful routes (index, create, store, show, edit,
|
|
296
|
+
* update, destroy — `update` registered for both PUT and PATCH) and exposes
|
|
297
|
+
* the fluent filtering/config API: `only`/`except`/`apiOnly`/`params`/`tap`/
|
|
298
|
+
* `where`/`use`/`middleware`/`as`. Nested resources use dot-notation
|
|
299
|
+
* (`posts.comments` → `/posts/:post_id/comments/:id`).
|
|
300
|
+
*/
|
|
301
|
+
export class RouteResource extends Macroable {
|
|
302
|
+
#resource;
|
|
303
|
+
#controller;
|
|
304
|
+
#shallow;
|
|
305
|
+
#register;
|
|
306
|
+
#markDirty;
|
|
307
|
+
/** Resource name → param placeholder (`comments` → `:id`, `posts` → `:post_id`). */
|
|
308
|
+
#params = {};
|
|
309
|
+
#baseName;
|
|
310
|
+
/** The generated routes, in declaration order (AdonisJS `resource.routes`). */
|
|
311
|
+
routes = [];
|
|
312
|
+
constructor(options) {
|
|
313
|
+
super();
|
|
314
|
+
const resource = options.resource.replace(/^\//, '').replace(/\/$/, '');
|
|
315
|
+
if (!resource)
|
|
316
|
+
throw new Error(`Invalid resource name "${options.resource}"`);
|
|
317
|
+
this.#resource = resource;
|
|
318
|
+
this.#controller = options.controller;
|
|
319
|
+
this.#shallow = options.shallow;
|
|
320
|
+
this.#register = options.register;
|
|
321
|
+
this.#markDirty = options.markDirty;
|
|
322
|
+
this.#baseName = resource.split('.').map(snakeCase).join('.');
|
|
323
|
+
this.#buildRoutes();
|
|
324
|
+
}
|
|
325
|
+
#createRoute(pattern, method, action) {
|
|
326
|
+
const def = makeDefinition(method, `/${pattern}`, [this.#controller, action]);
|
|
327
|
+
def.name = `${this.#baseName}.${action}`;
|
|
328
|
+
this.#register(def);
|
|
329
|
+
this.routes.push(new RouteBuilder(def));
|
|
330
|
+
}
|
|
331
|
+
#buildRoutes() {
|
|
332
|
+
const segments = this.#resource.split('.');
|
|
333
|
+
const main = segments.pop() ?? this.#resource;
|
|
334
|
+
this.#params[main] = ':id';
|
|
335
|
+
const parentPath = segments
|
|
336
|
+
.map((parent) => {
|
|
337
|
+
const param = `:${snakeCase(singular(parent))}_id`;
|
|
338
|
+
this.#params[parent] = param;
|
|
339
|
+
return `${parent}/${param}`;
|
|
340
|
+
})
|
|
341
|
+
.join('/');
|
|
342
|
+
const baseURI = parentPath ? `${parentPath}/${main}` : main;
|
|
343
|
+
// Shallow member routes drop the parent prefix — the id alone identifies
|
|
344
|
+
// the record (AdonisJS shallow resources).
|
|
345
|
+
const memberBase = this.#shallow ? main : baseURI;
|
|
346
|
+
this.#createRoute(baseURI, 'GET', 'index');
|
|
347
|
+
this.#createRoute(`${baseURI}/create`, 'GET', 'create');
|
|
348
|
+
this.#createRoute(baseURI, 'POST', 'store');
|
|
349
|
+
this.#createRoute(`${memberBase}/:id`, 'GET', 'show');
|
|
350
|
+
this.#createRoute(`${memberBase}/:id/edit`, 'GET', 'edit');
|
|
351
|
+
this.#createRoute(`${memberBase}/:id`, 'PUT', 'update');
|
|
352
|
+
this.#createRoute(`${memberBase}/:id`, 'PATCH', 'update');
|
|
353
|
+
this.#createRoute(`${memberBase}/:id`, 'DELETE', 'destroy');
|
|
354
|
+
}
|
|
355
|
+
/** Routes whose name ends with one of `names` (the action suffix). */
|
|
356
|
+
#matching(names) {
|
|
357
|
+
return this.routes.filter((route) => {
|
|
358
|
+
const name = route.getName();
|
|
359
|
+
return name !== undefined && names.some((action) => name.endsWith(action));
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/** Keep only the given actions; soft-delete the rest (AdonisJS `only`). */
|
|
363
|
+
only(names) {
|
|
364
|
+
const keep = new Set(this.#matching(names));
|
|
365
|
+
for (const route of this.routes)
|
|
366
|
+
if (!keep.has(route))
|
|
367
|
+
route.markAsDeleted();
|
|
368
|
+
this.#markDirty();
|
|
369
|
+
return this;
|
|
370
|
+
}
|
|
371
|
+
/** Register all actions except the given ones (AdonisJS `except`). */
|
|
372
|
+
except(names) {
|
|
373
|
+
for (const route of this.#matching(names))
|
|
374
|
+
route.markAsDeleted();
|
|
375
|
+
this.#markDirty();
|
|
376
|
+
return this;
|
|
377
|
+
}
|
|
378
|
+
/** Drop the form-rendering `create` + `edit` actions (AdonisJS `apiOnly`). */
|
|
379
|
+
apiOnly() {
|
|
380
|
+
return this.except(['create', 'edit']);
|
|
381
|
+
}
|
|
382
|
+
/** Constrain a param across every action route (AdonisJS `resource.where`). */
|
|
383
|
+
where(key, matcher) {
|
|
384
|
+
for (const route of this.routes)
|
|
385
|
+
route.where(key, matcher);
|
|
386
|
+
return this;
|
|
387
|
+
}
|
|
388
|
+
tap(actionsOrCallback, callback) {
|
|
389
|
+
if (typeof actionsOrCallback === 'function') {
|
|
390
|
+
for (const route of this.routes)
|
|
391
|
+
if (!route.isDeleted())
|
|
392
|
+
actionsOrCallback(route);
|
|
393
|
+
return this;
|
|
394
|
+
}
|
|
395
|
+
const names = Array.isArray(actionsOrCallback) ? actionsOrCallback : [actionsOrCallback];
|
|
396
|
+
if (callback) {
|
|
397
|
+
for (const route of this.#matching(names))
|
|
398
|
+
if (!route.isDeleted())
|
|
399
|
+
callback(route);
|
|
400
|
+
}
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
/** Rename the `:id` param of one or more resources (AdonisJS `params`). */
|
|
404
|
+
params(resources) {
|
|
405
|
+
for (const [resource, param] of Object.entries(resources)) {
|
|
406
|
+
const existing = this.#params[resource];
|
|
407
|
+
if (!existing)
|
|
408
|
+
continue;
|
|
409
|
+
this.#params[resource] = `:${param}`;
|
|
410
|
+
for (const route of this.routes) {
|
|
411
|
+
route.setPattern(route.getPattern().replace(`${resource}/${existing}`, `${resource}/:${param}`));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
this.#markDirty();
|
|
415
|
+
return this;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Attach middleware to specific actions (or `'*'` for all) — AdonisJS `use`.
|
|
419
|
+
* Named middleware are strings; inline middleware are functions.
|
|
420
|
+
*/
|
|
421
|
+
use(actions, middleware) {
|
|
422
|
+
const list = Array.isArray(middleware) ? middleware : [middleware];
|
|
423
|
+
const apply = (route) => {
|
|
424
|
+
for (const mw of list) {
|
|
425
|
+
if (typeof mw === 'string')
|
|
426
|
+
route.middleware(mw);
|
|
427
|
+
else
|
|
428
|
+
route.use(mw);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
if (actions === '*')
|
|
432
|
+
this.tap(apply);
|
|
433
|
+
else
|
|
434
|
+
this.tap(actions, apply);
|
|
435
|
+
return this;
|
|
436
|
+
}
|
|
437
|
+
/** Alias for {@link RouteResource.use} (AdonisJS `middleware`). */
|
|
438
|
+
middleware(actions, middleware) {
|
|
439
|
+
return this.use(actions, middleware);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Rename the base of every route name (AdonisJS `as`). `normalizeName`
|
|
443
|
+
* snake_cases the new base by default.
|
|
444
|
+
*/
|
|
445
|
+
as(name, normalizeName = true) {
|
|
446
|
+
const newBase = normalizeName ? snakeCase(name) : name;
|
|
447
|
+
for (const route of this.routes) {
|
|
448
|
+
const current = route.getName();
|
|
449
|
+
if (current)
|
|
450
|
+
route.as(current.replace(this.#baseName, newBase));
|
|
451
|
+
}
|
|
452
|
+
this.#baseName = newBase;
|
|
453
|
+
this.#markDirty();
|
|
454
|
+
return this;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
166
457
|
function isViewEngine(value) {
|
|
167
458
|
return (value !== null &&
|
|
168
459
|
typeof value === 'object' &&
|
|
@@ -170,7 +461,7 @@ function isViewEngine(value) {
|
|
|
170
461
|
typeof value.render === 'function');
|
|
171
462
|
}
|
|
172
463
|
// ─── OnRouteBuilder ─────────────────────────────────────────
|
|
173
|
-
/** Builder for on(path).render(view) and on(path).redirect(
|
|
464
|
+
/** Builder for on(path).render(view) and on(path).redirect(namedRoute). */
|
|
174
465
|
export class OnRouteBuilder {
|
|
175
466
|
#router;
|
|
176
467
|
#path;
|
|
@@ -190,26 +481,34 @@ export class OnRouteBuilder {
|
|
|
190
481
|
ctx.response.type('text/html; charset=utf-8').send(html);
|
|
191
482
|
});
|
|
192
483
|
}
|
|
193
|
-
/**
|
|
194
|
-
|
|
484
|
+
/**
|
|
485
|
+
* Redirect to a NAMED route (AdonisJS brisk `redirect`). Mirrors AdonisJS:
|
|
486
|
+
* the first argument is a route name, not a path — use {@link redirectToPath}
|
|
487
|
+
* for a fixed URL.
|
|
488
|
+
*/
|
|
489
|
+
redirect(name, params, status = 302) {
|
|
195
490
|
return this.#router.get(this.#path, async (ctx) => {
|
|
196
|
-
ctx.response.redirect().status(status).
|
|
491
|
+
ctx.response.redirect().status(status).toRoute(name, params);
|
|
197
492
|
});
|
|
198
493
|
}
|
|
199
|
-
/** Redirect to a
|
|
200
|
-
|
|
494
|
+
/** Redirect to a fixed path/URL (AdonisJS brisk `redirectToPath`). */
|
|
495
|
+
redirectToPath(target, status = 302) {
|
|
201
496
|
return this.#router.get(this.#path, async (ctx) => {
|
|
202
|
-
ctx.response.redirect().status(status).
|
|
497
|
+
ctx.response.redirect().status(status).toPath(target);
|
|
203
498
|
});
|
|
204
499
|
}
|
|
205
500
|
}
|
|
206
501
|
// ─── Router ─────────────────────────────────────────────────
|
|
207
502
|
/** Main Router. */
|
|
208
|
-
export class Router {
|
|
503
|
+
export class Router extends Macroable {
|
|
209
504
|
#routes = [];
|
|
210
505
|
#globalMatchers = {};
|
|
211
506
|
#routerMiddleware = [];
|
|
212
507
|
#namedMiddleware = new Map();
|
|
508
|
+
/** Controller registry: name → lazy import, for string handler references. */
|
|
509
|
+
#controllers = new Map();
|
|
510
|
+
/** APP_KEY-backed signed-URL helper (injected by the Ignitor when set). */
|
|
511
|
+
#signedUrl;
|
|
213
512
|
/** Index: static routes by "METHOD:path" for O(1) exact match. */
|
|
214
513
|
#staticIndex = new Map();
|
|
215
514
|
/** Index: parametric routes grouped by method. */
|
|
@@ -232,19 +531,46 @@ export class Router {
|
|
|
232
531
|
return this;
|
|
233
532
|
}
|
|
234
533
|
/**
|
|
235
|
-
* Register named middleware collection
|
|
534
|
+
* Register a named middleware collection and return per-name FACTORIES
|
|
535
|
+
* (AdonisJS `defineNamedMiddleware`). Call a factory to bind per-route
|
|
536
|
+
* arguments, then attach the result inline:
|
|
537
|
+
*
|
|
236
538
|
* export const middleware = router.named({
|
|
237
539
|
* auth: () => import('#middleware/auth_middleware'),
|
|
238
540
|
* })
|
|
541
|
+
* router.get('/admin', [AdminController, 'index']).use([middleware.auth({ guards: ['web'] })])
|
|
542
|
+
*
|
|
543
|
+
* By-name usage (`route.middleware('auth')`) still runs the middleware with
|
|
544
|
+
* no arguments — the factory form is only needed to pass `args` to `handle`.
|
|
239
545
|
*/
|
|
240
546
|
named(collection) {
|
|
241
|
-
const
|
|
547
|
+
const factories = {};
|
|
242
548
|
for (const [name, mw] of Object.entries(collection)) {
|
|
243
|
-
|
|
244
|
-
this.#namedMiddleware.set(name,
|
|
245
|
-
|
|
549
|
+
// By-name resolution (`route.middleware('auth')`) uses the no-arg form.
|
|
550
|
+
this.#namedMiddleware.set(name, resolveMiddlewareEntry(mw));
|
|
551
|
+
// The returned factory bakes in per-route args for inline `.use()`.
|
|
552
|
+
factories[name] = resolveParametrizedMiddlewareEntry(mw);
|
|
246
553
|
}
|
|
247
|
-
return
|
|
554
|
+
return factories;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Register controllers for magic string handler references (AdonisJS lazy
|
|
558
|
+
* controller pattern). Each entry maps a controller name to a lazy import:
|
|
559
|
+
*
|
|
560
|
+
* router.controllers({ UsersController: () => import('#controllers/users_controller') })
|
|
561
|
+
* router.get('/users', 'UsersController.index')
|
|
562
|
+
*
|
|
563
|
+
* The class is imported on first request and resolved through the IoC
|
|
564
|
+
* container (so @inject + @Guard/@Role decorators work). A string reference
|
|
565
|
+
* to a name that was never registered fails loudly when the route index is
|
|
566
|
+
* built.
|
|
567
|
+
*/
|
|
568
|
+
controllers(map) {
|
|
569
|
+
for (const [name, loader] of Object.entries(map)) {
|
|
570
|
+
this.#controllers.set(name, loader);
|
|
571
|
+
}
|
|
572
|
+
this.#indexDirty = true;
|
|
573
|
+
return this;
|
|
248
574
|
}
|
|
249
575
|
/** Get the router-level middleware stack. */
|
|
250
576
|
getRouterMiddleware() {
|
|
@@ -255,12 +581,20 @@ export class Router {
|
|
|
255
581
|
return this.#namedMiddleware.get(name);
|
|
256
582
|
}
|
|
257
583
|
// ─── Route registration ───────────────────────────────────
|
|
258
|
-
/**
|
|
584
|
+
/**
|
|
585
|
+
* Register a route for one verb or several (AdonisJS `route(pattern, methods)`
|
|
586
|
+
* accepts an array). A multi-verb call returns a single builder that
|
|
587
|
+
* configures every verb at once.
|
|
588
|
+
*/
|
|
259
589
|
route(method, path, handler) {
|
|
260
|
-
const
|
|
261
|
-
|
|
590
|
+
const methods = Array.isArray(method) ? method : [method];
|
|
591
|
+
const defs = methods.map((verb) => {
|
|
592
|
+
const def = this.#createDefinition(verb, path, handler);
|
|
593
|
+
this.#routes.push(def);
|
|
594
|
+
return def;
|
|
595
|
+
});
|
|
262
596
|
this.#indexDirty = true;
|
|
263
|
-
return new RouteBuilder(
|
|
597
|
+
return new RouteBuilder(defs);
|
|
264
598
|
}
|
|
265
599
|
get(path, handler) {
|
|
266
600
|
return this.route('GET', path, handler);
|
|
@@ -289,37 +623,53 @@ export class Router {
|
|
|
289
623
|
}
|
|
290
624
|
// ─── Resource routes ──────────────────────────────────────
|
|
291
625
|
/**
|
|
292
|
-
* Register resourceful routes for a controller.
|
|
293
|
-
*
|
|
626
|
+
* Register resourceful routes for a controller (AdonisJS `resource`).
|
|
627
|
+
*
|
|
628
|
+
* Generates index / create / store / show / edit / update / destroy —
|
|
629
|
+
* `update` for both PUT and PATCH. Returns a {@link RouteResource} for
|
|
630
|
+
* fluent filtering: `.only()`, `.except()`, `.apiOnly()`, `.params()`,
|
|
631
|
+
* `.tap()`, `.where()`, `.use()`, `.as()`.
|
|
632
|
+
*
|
|
633
|
+
* Nested resources use dot-notation (`posts.comments` →
|
|
634
|
+
* `/posts/:post_id/comments/:id`).
|
|
294
635
|
*
|
|
295
636
|
* Usage:
|
|
296
637
|
* router.resource('posts', PostsController)
|
|
297
|
-
*
|
|
298
|
-
* // POST /posts → PostsController.store
|
|
299
|
-
* // GET /posts/:id → PostsController.show
|
|
300
|
-
* // PUT /posts/:id → PostsController.update
|
|
301
|
-
* // DELETE /posts/:id → PostsController.destroy
|
|
638
|
+
* router.resource('posts', PostsController).apiOnly()
|
|
302
639
|
*/
|
|
303
|
-
// biome-ignore lint/suspicious/noExplicitAny: see ControllerAction
|
|
304
640
|
resource(path, controller) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
641
|
+
return new RouteResource({
|
|
642
|
+
resource: path,
|
|
643
|
+
controller,
|
|
644
|
+
shallow: false,
|
|
645
|
+
register: (def) => {
|
|
646
|
+
this.#routes.push(def);
|
|
647
|
+
this.#indexDirty = true;
|
|
648
|
+
},
|
|
649
|
+
markDirty: () => {
|
|
650
|
+
this.#indexDirty = true;
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Register a shallow resource (AdonisJS `shallowResource`). Member routes
|
|
656
|
+
* (show/edit/update/destroy) drop the parent prefix, since the record id
|
|
657
|
+
* alone identifies it: `posts.comments` → `/comments/:id` for members but
|
|
658
|
+
* `/posts/:post_id/comments` for the collection.
|
|
659
|
+
*/
|
|
660
|
+
shallowResource(path, controller) {
|
|
661
|
+
return new RouteResource({
|
|
662
|
+
resource: path,
|
|
663
|
+
controller,
|
|
664
|
+
shallow: true,
|
|
665
|
+
register: (def) => {
|
|
666
|
+
this.#routes.push(def);
|
|
667
|
+
this.#indexDirty = true;
|
|
668
|
+
},
|
|
669
|
+
markDirty: () => {
|
|
670
|
+
this.#indexDirty = true;
|
|
671
|
+
},
|
|
672
|
+
});
|
|
323
673
|
}
|
|
324
674
|
group(callbackOrConfig, legacyCallback) {
|
|
325
675
|
if (typeof callbackOrConfig === 'function') {
|
|
@@ -353,7 +703,7 @@ export class Router {
|
|
|
353
703
|
// ─── Global matchers ──────────────────────────────────────
|
|
354
704
|
/** Set a global param matcher (applied to all routes). */
|
|
355
705
|
where(param, matcher) {
|
|
356
|
-
this.#globalMatchers[param] = matcher
|
|
706
|
+
this.#globalMatchers[param] = compileMatcher(matcher);
|
|
357
707
|
return this;
|
|
358
708
|
}
|
|
359
709
|
// ─── Route matching ───────────────────────────────────────
|
|
@@ -363,6 +713,19 @@ export class Router {
|
|
|
363
713
|
this.#paramIndex.clear();
|
|
364
714
|
this.#nameIndex.clear();
|
|
365
715
|
for (const route of this.#routes) {
|
|
716
|
+
// Soft-deleted routes (resource .only()/.except()/.apiOnly()) never match.
|
|
717
|
+
if (route.deleted)
|
|
718
|
+
continue;
|
|
719
|
+
// Resolve a string handler reference against the controller registry.
|
|
720
|
+
if (route.stringRef && !route.lazyController) {
|
|
721
|
+
const loader = this.#controllers.get(route.stringRef.controller);
|
|
722
|
+
if (!loader) {
|
|
723
|
+
throw new Error(`[E_UNREGISTERED_CONTROLLER] Route ${route.method} ${route.path} references ` +
|
|
724
|
+
`controller '${route.stringRef.controller}', which is not registered. Register it ` +
|
|
725
|
+
`with router.controllers({ ${route.stringRef.controller}: () => import('...') }).`);
|
|
726
|
+
}
|
|
727
|
+
route.lazyController = { loader, method: route.stringRef.method };
|
|
728
|
+
}
|
|
366
729
|
// Named route index
|
|
367
730
|
if (route.name) {
|
|
368
731
|
this.#nameIndex.set(route.name, route);
|
|
@@ -474,6 +837,29 @@ export class Router {
|
|
|
474
837
|
makeUrl(name, params) {
|
|
475
838
|
return this.urlFor(name, params);
|
|
476
839
|
}
|
|
840
|
+
/** @internal Inject the APP_KEY-backed signed-URL helper (wired by the Ignitor). */
|
|
841
|
+
setSignedUrl(signedUrl) {
|
|
842
|
+
this.#signedUrl = signedUrl;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Generate a tamper-proof signed URL for a named route (AdonisJS
|
|
846
|
+
* `makeSignedUrl`). Delegates to the {@link SignedUrl} helper: appends an HMAC
|
|
847
|
+
* `signature`, plus a signed `expires` timestamp when `expiresIn` is given.
|
|
848
|
+
* Verified on the request side by `request.hasValidSignature()`.
|
|
849
|
+
*
|
|
850
|
+
* Requires an APP_KEY-backed signer (registered when `APP_KEY` is set).
|
|
851
|
+
*/
|
|
852
|
+
makeSignedUrl(name, params, options) {
|
|
853
|
+
if (!this.#signedUrl) {
|
|
854
|
+
throw new Error('[E_NO_APP_KEY] makeSignedUrl() requires an APP_KEY-backed signer. Set APP_KEY (>= 16 chars) so the encryption service is registered.');
|
|
855
|
+
}
|
|
856
|
+
const path = this.urlFor(name, params);
|
|
857
|
+
const withQs = options?.qs ? `${path}?${new URLSearchParams(options.qs).toString()}` : path;
|
|
858
|
+
return this.#signedUrl.make(withQs, {
|
|
859
|
+
expiresIn: options?.expiresIn,
|
|
860
|
+
purpose: options?.purpose,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
477
863
|
/**
|
|
478
864
|
* Map of every NAMED route's `name` → path pattern, e.g.
|
|
479
865
|
* `{ 'users.show': '/users/:id' }`. Serialize this into a page so a
|
|
@@ -490,10 +876,59 @@ export class Router {
|
|
|
490
876
|
}
|
|
491
877
|
return manifest;
|
|
492
878
|
}
|
|
879
|
+
/**
|
|
880
|
+
* Generate a TypeScript source string typing every named route and its
|
|
881
|
+
* params (AdonisJS `generateTypes`) — a `RouteName` union plus a
|
|
882
|
+
* `RouteParams` map inferred from each pattern's `:param` segments. Write it
|
|
883
|
+
* to a `.d.ts` so `urlFor(name, params)` is type-checked. Returns the source;
|
|
884
|
+
* the caller decides where to persist it.
|
|
885
|
+
*/
|
|
886
|
+
generateTypes() {
|
|
887
|
+
if (this.#indexDirty)
|
|
888
|
+
this.#buildIndex();
|
|
889
|
+
const names = [...this.#nameIndex.keys()].sort();
|
|
890
|
+
if (names.length === 0) {
|
|
891
|
+
return 'export type RouteName = never\nexport interface RouteParams {}\n';
|
|
892
|
+
}
|
|
893
|
+
const union = names.map((name) => ` | ${JSON.stringify(name)}`).join('\n');
|
|
894
|
+
const entries = names.map((name) => {
|
|
895
|
+
const pattern = this.#nameIndex.get(name)?.path ?? '';
|
|
896
|
+
const params = [...pattern.matchAll(/:([A-Za-z_]\w*)(\??)/g)];
|
|
897
|
+
const shape = params.length === 0
|
|
898
|
+
? 'Record<string, never>'
|
|
899
|
+
: `{ ${params.map((m) => `${JSON.stringify(m[1])}${m[2] === '?' ? '?' : ''}: string`).join('; ')} }`;
|
|
900
|
+
return ` ${JSON.stringify(name)}: ${shape}`;
|
|
901
|
+
});
|
|
902
|
+
return `export type RouteName =\n${union}\n\nexport interface RouteParams {\n${entries.join('\n')}\n}\n`;
|
|
903
|
+
}
|
|
904
|
+
// ─── Lookup ───────────────────────────────────────────────
|
|
905
|
+
/**
|
|
906
|
+
* Find a registered route by name or exact pattern (AdonisJS `router.find`).
|
|
907
|
+
* Returns null when nothing matches. Soft-deleted routes are ignored.
|
|
908
|
+
*/
|
|
909
|
+
find(identifier) {
|
|
910
|
+
if (this.#indexDirty)
|
|
911
|
+
this.#buildIndex();
|
|
912
|
+
const byName = this.#nameIndex.get(identifier);
|
|
913
|
+
if (byName)
|
|
914
|
+
return byName;
|
|
915
|
+
return this.#routes.find((route) => !route.deleted && route.path === identifier) ?? null;
|
|
916
|
+
}
|
|
917
|
+
/** Like {@link find} but throws when the route is absent (AdonisJS `findOrFail`). */
|
|
918
|
+
findOrFail(identifier) {
|
|
919
|
+
const route = this.find(identifier);
|
|
920
|
+
if (!route)
|
|
921
|
+
throw new Error(`Cannot find route for '${identifier}'`);
|
|
922
|
+
return route;
|
|
923
|
+
}
|
|
924
|
+
/** True when a route with the given name or pattern exists (AdonisJS `router.has`). */
|
|
925
|
+
has(identifier) {
|
|
926
|
+
return this.find(identifier) !== null;
|
|
927
|
+
}
|
|
493
928
|
// ─── Accessors ────────────────────────────────────────────
|
|
494
|
-
/** Get all registered routes (for OpenAPI generation, introspection). */
|
|
929
|
+
/** Get all registered (non-deleted) routes (for OpenAPI generation, introspection). */
|
|
495
930
|
getRoutes() {
|
|
496
|
-
return
|
|
931
|
+
return this.#routes.filter((route) => !route.deleted);
|
|
497
932
|
}
|
|
498
933
|
/**
|
|
499
934
|
* Clear all registered routes AND registries (used by hot-reload). Router
|
|
@@ -505,6 +940,7 @@ export class Router {
|
|
|
505
940
|
this.#routes = [];
|
|
506
941
|
this.#routerMiddleware = [];
|
|
507
942
|
this.#namedMiddleware.clear();
|
|
943
|
+
this.#controllers.clear();
|
|
508
944
|
this.#globalMatchers = {};
|
|
509
945
|
this.#indexDirty = true;
|
|
510
946
|
}
|
|
@@ -513,25 +949,7 @@ export class Router {
|
|
|
513
949
|
}
|
|
514
950
|
// ─── Internals ────────────────────────────────────────────
|
|
515
951
|
#createDefinition(method, path, handler) {
|
|
516
|
-
|
|
517
|
-
method: method.toUpperCase(),
|
|
518
|
-
path,
|
|
519
|
-
handler: null,
|
|
520
|
-
middleware: [],
|
|
521
|
-
inlineMiddleware: [],
|
|
522
|
-
guards: [],
|
|
523
|
-
roles: [],
|
|
524
|
-
permissions: [],
|
|
525
|
-
validators: [],
|
|
526
|
-
matchers: {},
|
|
527
|
-
};
|
|
528
|
-
if (Array.isArray(handler)) {
|
|
529
|
-
def.controller = { target: handler[0], method: handler[1] };
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
def.handler = handler;
|
|
533
|
-
}
|
|
534
|
-
return def;
|
|
952
|
+
return makeDefinition(method, path, handler);
|
|
535
953
|
}
|
|
536
954
|
#validateMatchers(params, matcherMap) {
|
|
537
955
|
for (const [param, value] of Object.entries(params)) {
|