@c9up/ream 0.1.25 → 0.1.27

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.
Files changed (302) hide show
  1. package/README.md +65 -0
  2. package/dist/ConfigLoader.d.ts +46 -5
  3. package/dist/ConfigLoader.d.ts.map +1 -1
  4. package/dist/ConfigLoader.js +60 -9
  5. package/dist/ConfigLoader.js.map +1 -1
  6. package/dist/HttpKernel.d.ts.map +1 -1
  7. package/dist/HttpKernel.js +46 -6
  8. package/dist/HttpKernel.js.map +1 -1
  9. package/dist/Ignitor.d.ts +115 -7
  10. package/dist/Ignitor.d.ts.map +1 -1
  11. package/dist/Ignitor.js +254 -40
  12. package/dist/Ignitor.js.map +1 -1
  13. package/dist/console/BaseCommand.d.ts +217 -0
  14. package/dist/console/BaseCommand.d.ts.map +1 -0
  15. package/dist/console/BaseCommand.js +340 -0
  16. package/dist/console/BaseCommand.js.map +1 -0
  17. package/dist/console/Console.d.ts +86 -0
  18. package/dist/console/Console.d.ts.map +1 -0
  19. package/dist/console/Console.js +134 -0
  20. package/dist/console/Console.js.map +1 -0
  21. package/dist/console/ExceptionHandler.d.ts +28 -0
  22. package/dist/console/ExceptionHandler.d.ts.map +1 -0
  23. package/dist/console/ExceptionHandler.js +76 -0
  24. package/dist/console/ExceptionHandler.js.map +1 -0
  25. package/dist/console/HelpCommand.d.ts +16 -0
  26. package/dist/console/HelpCommand.d.ts.map +1 -0
  27. package/dist/console/HelpCommand.js +40 -0
  28. package/dist/console/HelpCommand.js.map +1 -0
  29. package/dist/console/IndexGenerator.d.ts +24 -0
  30. package/dist/console/IndexGenerator.d.ts.map +1 -0
  31. package/dist/console/IndexGenerator.js +65 -0
  32. package/dist/console/IndexGenerator.js.map +1 -0
  33. package/dist/console/Kernel.d.ts +298 -0
  34. package/dist/console/Kernel.d.ts.map +1 -0
  35. package/dist/console/Kernel.js +1120 -0
  36. package/dist/console/Kernel.js.map +1 -0
  37. package/dist/console/ListCommand.d.ts +18 -0
  38. package/dist/console/ListCommand.d.ts.map +1 -0
  39. package/dist/console/ListCommand.js +60 -0
  40. package/dist/console/ListCommand.js.map +1 -0
  41. package/dist/console/assertions.d.ts +20 -0
  42. package/dist/console/assertions.d.ts.map +1 -0
  43. package/dist/console/assertions.js +84 -0
  44. package/dist/console/assertions.js.map +1 -0
  45. package/dist/console/cliui.d.ts +248 -0
  46. package/dist/console/cliui.d.ts.map +1 -0
  47. package/dist/console/cliui.js +587 -0
  48. package/dist/console/cliui.js.map +1 -0
  49. package/dist/console/decorators.d.ts +90 -0
  50. package/dist/console/decorators.d.ts.map +1 -0
  51. package/dist/console/decorators.js +142 -0
  52. package/dist/console/decorators.js.map +1 -0
  53. package/dist/console/index.d.ts +32 -0
  54. package/dist/console/index.d.ts.map +1 -0
  55. package/dist/console/index.js +24 -0
  56. package/dist/console/index.js.map +1 -0
  57. package/dist/console/loaders.d.ts +52 -0
  58. package/dist/console/loaders.d.ts.map +1 -0
  59. package/dist/console/loaders.js +131 -0
  60. package/dist/console/loaders.js.map +1 -0
  61. package/dist/console/parser.d.ts +86 -0
  62. package/dist/console/parser.d.ts.map +1 -0
  63. package/dist/console/parser.js +440 -0
  64. package/dist/console/parser.js.map +1 -0
  65. package/dist/console/prompts.d.ts +124 -0
  66. package/dist/console/prompts.d.ts.map +1 -0
  67. package/dist/console/prompts.js +356 -0
  68. package/dist/console/prompts.js.map +1 -0
  69. package/dist/console/tracing.d.ts +17 -0
  70. package/dist/console/tracing.d.ts.map +1 -0
  71. package/dist/console/tracing.js +11 -0
  72. package/dist/console/tracing.js.map +1 -0
  73. package/dist/console/types.d.ts +255 -0
  74. package/dist/console/types.d.ts.map +1 -0
  75. package/dist/console/types.js +25 -0
  76. package/dist/console/types.js.map +1 -0
  77. package/dist/console/ui.d.ts +20 -0
  78. package/dist/console/ui.d.ts.map +1 -0
  79. package/dist/console/ui.js +31 -0
  80. package/dist/console/ui.js.map +1 -0
  81. package/dist/console/utils.d.ts +27 -0
  82. package/dist/console/utils.d.ts.map +1 -0
  83. package/dist/console/utils.js +56 -0
  84. package/dist/console/utils.js.map +1 -0
  85. package/dist/container/Container.d.ts +23 -3
  86. package/dist/container/Container.d.ts.map +1 -1
  87. package/dist/container/Container.js +177 -70
  88. package/dist/container/Container.js.map +1 -1
  89. package/dist/container/types.d.ts +14 -2
  90. package/dist/container/types.d.ts.map +1 -1
  91. package/dist/decorators/Service.d.ts +7 -0
  92. package/dist/decorators/Service.d.ts.map +1 -1
  93. package/dist/decorators/Service.js +23 -3
  94. package/dist/decorators/Service.js.map +1 -1
  95. package/dist/env/Env.d.ts +29 -4
  96. package/dist/env/Env.d.ts.map +1 -1
  97. package/dist/env/Env.js +27 -4
  98. package/dist/env/Env.js.map +1 -1
  99. package/dist/env/Secret.d.ts +11 -0
  100. package/dist/env/Secret.d.ts.map +1 -0
  101. package/dist/env/Secret.js +36 -0
  102. package/dist/env/Secret.js.map +1 -0
  103. package/dist/env/interpolate.d.ts +26 -0
  104. package/dist/env/interpolate.d.ts.map +1 -0
  105. package/dist/env/interpolate.js +48 -0
  106. package/dist/env/interpolate.js.map +1 -0
  107. package/dist/env/loadEnvFiles.d.ts.map +1 -1
  108. package/dist/env/loadEnvFiles.js +14 -4
  109. package/dist/env/loadEnvFiles.js.map +1 -1
  110. package/dist/env/schema.d.ts +23 -6
  111. package/dist/env/schema.d.ts.map +1 -1
  112. package/dist/env/schema.js +67 -33
  113. package/dist/env/schema.js.map +1 -1
  114. package/dist/errors/ReamError.d.ts +25 -0
  115. package/dist/errors/ReamError.d.ts.map +1 -1
  116. package/dist/errors/ReamError.js +5 -0
  117. package/dist/errors/ReamError.js.map +1 -1
  118. package/dist/errors/prettyPrintError.d.ts +9 -0
  119. package/dist/errors/prettyPrintError.d.ts.map +1 -0
  120. package/dist/errors/prettyPrintError.js +22 -0
  121. package/dist/errors/prettyPrintError.js.map +1 -0
  122. package/dist/events/Emitter.d.ts +1 -1
  123. package/dist/events/Emitter.d.ts.map +1 -1
  124. package/dist/events/Emitter.js +1 -1
  125. package/dist/events/Emitter.js.map +1 -1
  126. package/dist/events/EventsProvider.d.ts +2 -2
  127. package/dist/events/EventsProvider.d.ts.map +1 -1
  128. package/dist/events/EventsProvider.js +3 -3
  129. package/dist/events/EventsProvider.js.map +1 -1
  130. package/dist/graphql/GraphQLEngine.d.ts +1 -1
  131. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  132. package/dist/graphql/GraphQLEngine.js +2 -2
  133. package/dist/graphql/GraphQLEngine.js.map +1 -1
  134. package/dist/graphql/GraphQLProvider.d.ts.map +1 -1
  135. package/dist/graphql/GraphQLProvider.js +7 -3
  136. package/dist/graphql/GraphQLProvider.js.map +1 -1
  137. package/dist/http/Exception.d.ts +63 -1
  138. package/dist/http/Exception.d.ts.map +1 -1
  139. package/dist/http/Exception.js +185 -48
  140. package/dist/http/Exception.js.map +1 -1
  141. package/dist/http/HttpContext.d.ts +53 -7
  142. package/dist/http/HttpContext.d.ts.map +1 -1
  143. package/dist/http/HttpContext.js +56 -15
  144. package/dist/http/HttpContext.js.map +1 -1
  145. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  146. package/dist/http/RedirectBuilder.js +4 -1
  147. package/dist/http/RedirectBuilder.js.map +1 -1
  148. package/dist/http/Request.d.ts +141 -10
  149. package/dist/http/Request.d.ts.map +1 -1
  150. package/dist/http/Request.js +244 -34
  151. package/dist/http/Request.js.map +1 -1
  152. package/dist/http/Response.d.ts +176 -9
  153. package/dist/http/Response.d.ts.map +1 -1
  154. package/dist/http/Response.js +404 -16
  155. package/dist/http/Response.js.map +1 -1
  156. package/dist/index.d.ts +19 -8
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +20 -5
  159. package/dist/index.js.map +1 -1
  160. package/dist/middleware/Pipeline.d.ts +29 -8
  161. package/dist/middleware/Pipeline.d.ts.map +1 -1
  162. package/dist/middleware/Pipeline.js +40 -2
  163. package/dist/middleware/Pipeline.js.map +1 -1
  164. package/dist/openapi/OpenApiProvider.js +4 -4
  165. package/dist/openapi/OpenApiProvider.js.map +1 -1
  166. package/dist/router/Router.d.ts +223 -31
  167. package/dist/router/Router.d.ts.map +1 -1
  168. package/dist/router/Router.js +520 -102
  169. package/dist/router/Router.js.map +1 -1
  170. package/dist/rpc/RpcProvider.d.ts +1 -0
  171. package/dist/rpc/RpcProvider.d.ts.map +1 -1
  172. package/dist/rpc/RpcProvider.js +7 -3
  173. package/dist/rpc/RpcProvider.js.map +1 -1
  174. package/dist/rpc/RpcRouter.d.ts +2 -2
  175. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  176. package/dist/rpc/RpcRouter.js +17 -5
  177. package/dist/rpc/RpcRouter.js.map +1 -1
  178. package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
  179. package/dist/scheduler/ScheduleProvider.js +7 -15
  180. package/dist/scheduler/ScheduleProvider.js.map +1 -1
  181. package/dist/server/Server.d.ts +17 -4
  182. package/dist/server/Server.d.ts.map +1 -1
  183. package/dist/server/Server.js +41 -7
  184. package/dist/server/Server.js.map +1 -1
  185. package/dist/services/app.d.ts +1 -1
  186. package/dist/services/app.js +1 -1
  187. package/dist/services/console.d.ts +21 -0
  188. package/dist/services/console.d.ts.map +1 -0
  189. package/dist/services/console.js +35 -0
  190. package/dist/services/console.js.map +1 -0
  191. package/dist/session/SessionMiddleware.js +3 -3
  192. package/dist/session/SessionMiddleware.js.map +1 -1
  193. package/dist/storage/StaticProvider.js +1 -1
  194. package/dist/storage/StaticProvider.js.map +1 -1
  195. package/dist/testing/ApiResponse.d.ts +218 -0
  196. package/dist/testing/ApiResponse.d.ts.map +1 -0
  197. package/dist/testing/ApiResponse.js +576 -0
  198. package/dist/testing/ApiResponse.js.map +1 -0
  199. package/dist/testing/RequestBuilder.d.ts +155 -35
  200. package/dist/testing/RequestBuilder.d.ts.map +1 -1
  201. package/dist/testing/RequestBuilder.js +455 -109
  202. package/dist/testing/RequestBuilder.js.map +1 -1
  203. package/dist/testing/TestClient.d.ts +53 -37
  204. package/dist/testing/TestClient.d.ts.map +1 -1
  205. package/dist/testing/TestClient.js +84 -52
  206. package/dist/testing/TestClient.js.map +1 -1
  207. package/dist/utils/Macroable.d.ts +30 -0
  208. package/dist/utils/Macroable.d.ts.map +1 -0
  209. package/dist/utils/Macroable.js +43 -0
  210. package/dist/utils/Macroable.js.map +1 -0
  211. package/dist/utils/inflect.d.ts +17 -0
  212. package/dist/utils/inflect.d.ts.map +1 -0
  213. package/dist/utils/inflect.js +32 -0
  214. package/dist/utils/inflect.js.map +1 -0
  215. package/dist/utils/objectPath.d.ts +57 -0
  216. package/dist/utils/objectPath.d.ts.map +1 -0
  217. package/dist/utils/objectPath.js +141 -0
  218. package/dist/utils/objectPath.js.map +1 -0
  219. package/events.darwin-arm64.node +0 -0
  220. package/events.darwin-x64.node +0 -0
  221. package/events.linux-arm64-gnu.node +0 -0
  222. package/events.linux-x64-gnu.node +0 -0
  223. package/events.win32-x64-msvc.node +0 -0
  224. package/index.darwin-arm64.node +0 -0
  225. package/index.darwin-x64.node +0 -0
  226. package/index.linux-arm64-gnu.node +0 -0
  227. package/index.linux-x64-gnu.node +0 -0
  228. package/index.win32-x64-msvc.node +0 -0
  229. package/package.json +20 -2
  230. package/scheduler.darwin-arm64.node +0 -0
  231. package/scheduler.darwin-x64.node +0 -0
  232. package/scheduler.linux-arm64-gnu.node +0 -0
  233. package/scheduler.linux-x64-gnu.node +0 -0
  234. package/scheduler.win32-x64-msvc.node +0 -0
  235. package/src/ConfigLoader.ts +72 -9
  236. package/src/HttpKernel.ts +53 -15
  237. package/src/Ignitor.ts +364 -45
  238. package/src/console/BaseCommand.ts +467 -0
  239. package/src/console/Console.ts +174 -0
  240. package/src/console/ExceptionHandler.ts +91 -0
  241. package/src/console/HelpCommand.ts +37 -0
  242. package/src/console/IndexGenerator.ts +79 -0
  243. package/src/console/Kernel.ts +1382 -0
  244. package/src/console/ListCommand.ts +55 -0
  245. package/src/console/assertions.ts +114 -0
  246. package/src/console/cliui.ts +756 -0
  247. package/src/console/decorators.ts +199 -0
  248. package/src/console/index.ts +67 -0
  249. package/src/console/loaders.ts +145 -0
  250. package/src/console/parser.ts +598 -0
  251. package/src/console/prompts.ts +548 -0
  252. package/src/console/tracing.ts +22 -0
  253. package/src/console/types.ts +280 -0
  254. package/src/console/ui.ts +37 -0
  255. package/src/console/utils.ts +69 -0
  256. package/src/container/Container.ts +198 -72
  257. package/src/container/types.ts +15 -2
  258. package/src/decorators/Service.ts +28 -3
  259. package/src/env/Env.ts +49 -5
  260. package/src/env/Secret.ts +41 -0
  261. package/src/env/interpolate.ts +54 -0
  262. package/src/env/loadEnvFiles.ts +12 -4
  263. package/src/env/schema.ts +109 -35
  264. package/src/errors/ReamError.ts +31 -0
  265. package/src/errors/prettyPrintError.ts +20 -0
  266. package/src/events/Emitter.ts +2 -2
  267. package/src/events/EventsProvider.ts +5 -5
  268. package/src/graphql/GraphQLEngine.ts +4 -4
  269. package/src/graphql/GraphQLProvider.ts +7 -3
  270. package/src/http/Exception.ts +214 -37
  271. package/src/http/HttpContext.ts +87 -18
  272. package/src/http/RedirectBuilder.ts +4 -1
  273. package/src/http/Request.ts +264 -34
  274. package/src/http/Response.ts +478 -27
  275. package/src/index.ts +41 -6
  276. package/src/middleware/Pipeline.ts +77 -6
  277. package/src/openapi/OpenApiProvider.ts +4 -4
  278. package/src/router/Router.ts +631 -113
  279. package/src/rpc/RpcProvider.ts +6 -2
  280. package/src/rpc/RpcRouter.ts +22 -7
  281. package/src/scheduler/ScheduleProvider.ts +7 -15
  282. package/src/server/Server.ts +49 -10
  283. package/src/services/app.ts +1 -1
  284. package/src/services/console.ts +42 -0
  285. package/src/session/SessionMiddleware.ts +4 -4
  286. package/src/storage/StaticProvider.ts +1 -1
  287. package/src/testing/ApiResponse.ts +691 -0
  288. package/src/testing/RequestBuilder.ts +577 -137
  289. package/src/testing/TestClient.ts +110 -77
  290. package/src/utils/Macroable.ts +54 -0
  291. package/src/utils/inflect.ts +29 -0
  292. package/src/utils/objectPath.ts +142 -0
  293. package/dist/console/CommandRunner.d.ts +0 -18
  294. package/dist/console/CommandRunner.d.ts.map +0 -1
  295. package/dist/console/CommandRunner.js +0 -65
  296. package/dist/console/CommandRunner.js.map +0 -1
  297. package/dist/decorators/Lazy.d.ts +0 -19
  298. package/dist/decorators/Lazy.d.ts.map +0 -1
  299. package/dist/decorators/Lazy.js +0 -53
  300. package/dist/decorators/Lazy.js.map +0 -1
  301. package/src/console/CommandRunner.ts +0 -82
  302. package/src/decorators/Lazy.ts +0 -57
@@ -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
- /** Fluent route builder — mutates the already-registered route definition. */
25
- export class RouteBuilder {
26
- #route;
27
- constructor(route) {
28
- this.#route = route;
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.name = name;
33
- return this;
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.middleware.push(...names);
38
- return this;
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.inlineMiddleware.push(...mw);
43
- return this;
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.guards.push(...guards);
48
- return this;
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.roles.push(...roles);
53
- return this;
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.permissions.push(...permissions);
58
- return this;
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
- this.#route.matchers[param] = matcher instanceof RegExp ? { pattern: matcher } : matcher;
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.validators.push(validator);
82
- return this;
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.version = v;
87
- return this;
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.domain = d;
92
- return this;
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.deprecates = { version, sunset: options?.sunset };
97
- return this;
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
- /** @internal Get the underlying definition. */
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.#route;
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
- /** Prefix route names for all routes in the group. */
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
- route.name = `${namePrefix}.${route.name}`;
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 instanceof RegExp ? { pattern: matcher } : 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(target). */
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
- /** Redirect to a path. */
194
- redirect(target, status = 302) {
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).toPath(target);
491
+ ctx.response.redirect().status(status).toRoute(name, params);
197
492
  });
198
493
  }
199
- /** Redirect to a named route. */
200
- redirectToRoute(name, params, status = 302) {
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).toRoute(name, params);
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 resolved = {};
547
+ const factories = {};
242
548
  for (const [name, mw] of Object.entries(collection)) {
243
- const fn = resolveMiddlewareEntry(mw);
244
- this.#namedMiddleware.set(name, fn);
245
- resolved[name] = fn;
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 resolved;
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
- /** Register a route with any HTTP method. */
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 def = this.#createDefinition(method, path, handler);
261
- this.#routes.push(def);
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(def);
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
- * Generates: index, store, show, update, destroy
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
- * // GET /posts → PostsController.index
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
- const baseName = path.replace(/\//g, '.');
306
- const routes = [];
307
- const actions = [
308
- { method: 'GET', suffix: '', action: 'index', nameSuffix: 'index' },
309
- { method: 'POST', suffix: '', action: 'store', nameSuffix: 'store' },
310
- { method: 'GET', suffix: '/:id', action: 'show', nameSuffix: 'show' },
311
- { method: 'PUT', suffix: '/:id', action: 'update', nameSuffix: 'update' },
312
- { method: 'PATCH', suffix: '/:id', action: 'update', nameSuffix: 'update' },
313
- { method: 'DELETE', suffix: '/:id', action: 'destroy', nameSuffix: 'destroy' },
314
- ];
315
- for (const { method, suffix, action, nameSuffix } of actions) {
316
- const def = this.#createDefinition(method, `/${path}${suffix}`, [controller, action]);
317
- def.name = `${baseName}.${nameSuffix}`;
318
- routes.push(def);
319
- this.#routes.push(def);
320
- }
321
- this.#indexDirty = true;
322
- return new GroupBuilder(routes);
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 instanceof RegExp ? { pattern: matcher } : 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 [...this.#routes];
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
- const def = {
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)) {