keryx 0.21.7 → 0.22.0
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/classes/Action.ts +7 -5
- package/classes/Router.ts +157 -0
- package/initializers/actionts.ts +9 -0
- package/initializers/resque.ts +26 -15
- package/package.json +4 -2
- package/templates/scaffold/middleware-session.ts.mustache +1 -1
- package/testing/index.ts +105 -0
- package/util/webRouting.ts +11 -39
package/classes/Action.ts
CHANGED
|
@@ -181,10 +181,12 @@ export abstract class Action {
|
|
|
181
181
|
* action's `inputs` Zod schema (falls back to `Record<string, unknown>` when no schema is
|
|
182
182
|
* defined). By the time `run` is called, all middleware `runBefore` hooks have already
|
|
183
183
|
* executed and may have mutated the params.
|
|
184
|
-
* @param connection - The connection that initiated this action.
|
|
185
|
-
* caller's session (`connection.session`), subscription state, and raw
|
|
186
|
-
*
|
|
187
|
-
*
|
|
184
|
+
* @param connection - The connection that initiated this action. Always defined. Provides
|
|
185
|
+
* access to the caller's session (`connection.session`), subscription state, and raw
|
|
186
|
+
* transport handle. For background tasks (resque worker), `connection.type === "task"`
|
|
187
|
+
* and `connection.session.data` is empty — tasks are fresh starts, so actions that
|
|
188
|
+
* need user context should receive it as an input parameter rather than reading from
|
|
189
|
+
* the session.
|
|
188
190
|
* @param abortSignal - An `AbortSignal` tied to the action's timeout. The signal is aborted
|
|
189
191
|
* when the per-action `timeout` (or the global `config.actions.timeout`, default 300 000 ms)
|
|
190
192
|
* elapses. Long-running actions should check `abortSignal.aborted` or pass the signal to
|
|
@@ -194,7 +196,7 @@ export abstract class Action {
|
|
|
194
196
|
*/
|
|
195
197
|
abstract run(
|
|
196
198
|
params: ActionParams<Action>,
|
|
197
|
-
connection
|
|
199
|
+
connection: Connection,
|
|
198
200
|
abortSignal?: AbortSignal,
|
|
199
201
|
): Promise<any>;
|
|
200
202
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Action, HTTP_METHOD } from "./Action";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Result of a successful route match.
|
|
5
|
+
*/
|
|
6
|
+
export type RouterMatch = {
|
|
7
|
+
actionName: string;
|
|
8
|
+
pathParams?: Record<string, string>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type DynamicRoute = {
|
|
12
|
+
matcher: RegExp;
|
|
13
|
+
paramNames: string[];
|
|
14
|
+
actionName: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fast path-to-action lookup.
|
|
19
|
+
*
|
|
20
|
+
* `compile()` builds two lookup structures from a list of actions:
|
|
21
|
+
* - a static index keyed by method + exact path for plain-string routes with no `:param` segments
|
|
22
|
+
* - a dynamic list (per method) of pre-compiled regexes for `:param` and `RegExp` routes
|
|
23
|
+
*
|
|
24
|
+
* `match()` performs an O(1) static lookup first, then falls back to an O(k) scan of
|
|
25
|
+
* dynamic routes for that method (where k is the number of dynamic routes per method).
|
|
26
|
+
*
|
|
27
|
+
* The router preserves the registration order of dynamic routes, so when two dynamic
|
|
28
|
+
* patterns can both match a path, the first one registered wins — matching the
|
|
29
|
+
* behavior of the pre-router iteration loop.
|
|
30
|
+
*
|
|
31
|
+
* The router reads the action list through a source function supplied to
|
|
32
|
+
* `compile()`. On each `match()` call it checks whether the returned array
|
|
33
|
+
* reference or length has changed since the last build, and rebuilds on drift.
|
|
34
|
+
* This preserves the live-iteration semantics of the previous loop-based matcher
|
|
35
|
+
* — including tolerating tests that both push new actions onto
|
|
36
|
+
* `api.actions.actions` and that swap the array wholesale via
|
|
37
|
+
* `api.actions.actions = api.actions.actions.filter(...)`.
|
|
38
|
+
*/
|
|
39
|
+
export class Router {
|
|
40
|
+
private staticIndex: Map<HTTP_METHOD, Map<string, string>> = new Map();
|
|
41
|
+
private dynamicList: Map<HTTP_METHOD, DynamicRoute[]> = new Map();
|
|
42
|
+
private source: () => Action[] = () => [];
|
|
43
|
+
private lastSeen: Action[] | null = null;
|
|
44
|
+
private lastLength = -1;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Bind the router to an action source and eagerly build the lookup structures.
|
|
48
|
+
*
|
|
49
|
+
* The `source` argument may be the action array directly, or a getter function
|
|
50
|
+
* that returns the current array. A getter is the recommended form for
|
|
51
|
+
* long-lived routers (e.g. the one on `api.actions`) because some callers
|
|
52
|
+
* swap the underlying array reference rather than mutating in place.
|
|
53
|
+
*
|
|
54
|
+
* Safe to call multiple times — each call fully replaces the previous state.
|
|
55
|
+
* When in-process hot-reload for actions is added, callers must re-invoke this
|
|
56
|
+
* method; the currently relied-on dev restart via `bun --watch` is a full
|
|
57
|
+
* process restart, so `initialize()` naturally re-runs this.
|
|
58
|
+
*
|
|
59
|
+
* @param source - Either the actions array or a function returning it. Actions without a `web.route` are skipped.
|
|
60
|
+
*/
|
|
61
|
+
compile(source: Action[] | (() => Action[])): void {
|
|
62
|
+
this.source =
|
|
63
|
+
typeof source === "function" ? source : () => source as Action[];
|
|
64
|
+
|
|
65
|
+
// Eagerly build if the source is ready. If the getter throws (e.g. called
|
|
66
|
+
// inside `initialize()` before `api.actions` is assigned), defer to the
|
|
67
|
+
// first `match()` call.
|
|
68
|
+
try {
|
|
69
|
+
const actions = this.source();
|
|
70
|
+
this.rebuild(actions);
|
|
71
|
+
} catch {
|
|
72
|
+
this.lastSeen = null;
|
|
73
|
+
this.lastLength = -1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private rebuild(actions: Action[]): void {
|
|
78
|
+
this.staticIndex = new Map();
|
|
79
|
+
this.dynamicList = new Map();
|
|
80
|
+
|
|
81
|
+
for (const action of actions) {
|
|
82
|
+
if (!action?.web?.route) continue;
|
|
83
|
+
const { route, method } = action.web;
|
|
84
|
+
|
|
85
|
+
const isRegExp = route instanceof RegExp;
|
|
86
|
+
const hasParams = !isRegExp && /:\w+/.test(route as string);
|
|
87
|
+
|
|
88
|
+
if (!isRegExp && !hasParams) {
|
|
89
|
+
let byPath = this.staticIndex.get(method);
|
|
90
|
+
if (!byPath) {
|
|
91
|
+
byPath = new Map();
|
|
92
|
+
this.staticIndex.set(method, byPath);
|
|
93
|
+
}
|
|
94
|
+
if (!byPath.has(route as string)) {
|
|
95
|
+
byPath.set(route as string, action.name);
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const matcher = isRegExp
|
|
101
|
+
? (route as RegExp)
|
|
102
|
+
: new RegExp(`^${(route as string).replace(/:\w+/g, "([^/]+)")}$`);
|
|
103
|
+
const paramNames = isRegExp
|
|
104
|
+
? []
|
|
105
|
+
: ((route as string).match(/:\w+/g) ?? []).map((n) => n.slice(1));
|
|
106
|
+
|
|
107
|
+
let list = this.dynamicList.get(method);
|
|
108
|
+
if (!list) {
|
|
109
|
+
list = [];
|
|
110
|
+
this.dynamicList.set(method, list);
|
|
111
|
+
}
|
|
112
|
+
list.push({ matcher, paramNames, actionName: action.name });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.lastSeen = actions;
|
|
116
|
+
this.lastLength = actions.length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Match a path + method against the compiled routes.
|
|
121
|
+
*
|
|
122
|
+
* @param path - The request path (already stripped of any API prefix).
|
|
123
|
+
* @param method - Uppercase HTTP method.
|
|
124
|
+
* @returns The matched action name plus any extracted path parameters, or `null` if no route matched.
|
|
125
|
+
*/
|
|
126
|
+
match(path: string, method: HTTP_METHOD): RouterMatch | null {
|
|
127
|
+
const actions = this.source();
|
|
128
|
+
if (actions !== this.lastSeen || actions.length !== this.lastLength) {
|
|
129
|
+
this.rebuild(actions);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const staticHit = this.staticIndex.get(method)?.get(path);
|
|
133
|
+
if (staticHit) return { actionName: staticHit };
|
|
134
|
+
|
|
135
|
+
const candidates = this.dynamicList.get(method);
|
|
136
|
+
if (!candidates) return null;
|
|
137
|
+
|
|
138
|
+
for (const { matcher, paramNames, actionName } of candidates) {
|
|
139
|
+
const result = matcher.exec(path);
|
|
140
|
+
if (!result) continue;
|
|
141
|
+
|
|
142
|
+
if (paramNames.length === 0) return { actionName };
|
|
143
|
+
|
|
144
|
+
const pathParams: Record<string, string> = {};
|
|
145
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
146
|
+
const value = result[i + 1];
|
|
147
|
+
if (value !== undefined) pathParams[paramNames[i]] = value;
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
actionName,
|
|
151
|
+
pathParams: Object.keys(pathParams).length > 0 ? pathParams : undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
package/initializers/actionts.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from "path";
|
|
|
4
4
|
import { api, logger } from "../api";
|
|
5
5
|
import { type Action, DEFAULT_QUEUE } from "../classes/Action";
|
|
6
6
|
import { Initializer } from "../classes/Initializer";
|
|
7
|
+
import { Router } from "../classes/Router";
|
|
7
8
|
import { ErrorType, TypedError } from "../classes/TypedError";
|
|
8
9
|
import { config } from "../config";
|
|
9
10
|
import { formatLoadedMessage } from "../util/config";
|
|
@@ -672,8 +673,16 @@ export class Actions extends Initializer {
|
|
|
672
673
|
}),
|
|
673
674
|
);
|
|
674
675
|
|
|
676
|
+
// Keep the router compile co-located with action assembly — if in-process
|
|
677
|
+
// hot-reload is ever added, this is the single seam that must re-fire.
|
|
678
|
+
// The getter lets the router track wholesale array replacement (e.g. tests
|
|
679
|
+
// that do `api.actions.actions = api.actions.actions.filter(...)`).
|
|
680
|
+
const router = new Router();
|
|
681
|
+
router.compile(() => api.actions.actions);
|
|
682
|
+
|
|
675
683
|
return {
|
|
676
684
|
actions,
|
|
685
|
+
router,
|
|
677
686
|
|
|
678
687
|
enqueue: this.enqueue,
|
|
679
688
|
fanOut: this.fanOut,
|
package/initializers/resque.ts
CHANGED
|
@@ -279,9 +279,11 @@ export class Resque extends Initializer {
|
|
|
279
279
|
};
|
|
280
280
|
|
|
281
281
|
/**
|
|
282
|
-
* Wrap an action as a node-resque job. Creates a
|
|
283
|
-
*
|
|
284
|
-
*
|
|
282
|
+
* Wrap an action as a node-resque job. Creates a fresh `Connection` of type `"task"`
|
|
283
|
+
* with an empty in-memory session stub (tasks are fresh starts — no Redis read/write
|
|
284
|
+
* for session), converts inputs to a plain object, and runs the action via
|
|
285
|
+
* `connection.act()`. Handles fan-out result/error collection and recurring task
|
|
286
|
+
* re-enqueue.
|
|
285
287
|
*/
|
|
286
288
|
wrapActionAsJob = (
|
|
287
289
|
action: Action,
|
|
@@ -291,18 +293,6 @@ export class Resque extends Initializer {
|
|
|
291
293
|
pluginOptions: {},
|
|
292
294
|
|
|
293
295
|
perform: async function (params: ActionParams<typeof action>) {
|
|
294
|
-
const connection = new Connection(
|
|
295
|
-
"resque",
|
|
296
|
-
`job:${api.process.name}:${SERVER_JOB_COUNTER++}}`,
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
const propagatedCorrelationId = params._correlationId as
|
|
300
|
-
| string
|
|
301
|
-
| undefined;
|
|
302
|
-
if (propagatedCorrelationId) {
|
|
303
|
-
connection.correlationId = propagatedCorrelationId;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
296
|
const plainParams: Record<string, unknown> =
|
|
307
297
|
typeof params === "object" && params !== null
|
|
308
298
|
? Object.fromEntries(
|
|
@@ -312,6 +302,27 @@ export class Resque extends Initializer {
|
|
|
312
302
|
)
|
|
313
303
|
: {};
|
|
314
304
|
|
|
305
|
+
const propagatedCorrelationId = plainParams._correlationId as
|
|
306
|
+
| string
|
|
307
|
+
| undefined;
|
|
308
|
+
|
|
309
|
+
const connection = new Connection(
|
|
310
|
+
"task",
|
|
311
|
+
`job:${api.process.name}:${SERVER_JOB_COUNTER++}`,
|
|
312
|
+
);
|
|
313
|
+
if (propagatedCorrelationId) {
|
|
314
|
+
connection.correlationId = propagatedCorrelationId;
|
|
315
|
+
}
|
|
316
|
+
// Synthesize an empty session in-memory — tasks are fresh starts; needed data
|
|
317
|
+
// must come through action params, not session state.
|
|
318
|
+
connection.session = {
|
|
319
|
+
id: `task:${connection.id}`,
|
|
320
|
+
cookieName: config.session.cookieName,
|
|
321
|
+
createdAt: Date.now(),
|
|
322
|
+
data: {},
|
|
323
|
+
};
|
|
324
|
+
connection.sessionLoaded = true;
|
|
325
|
+
|
|
315
326
|
const fanOutId = plainParams._fanOutId as string | undefined;
|
|
316
327
|
|
|
317
328
|
let response: Awaited<ReturnType<(typeof action)["run"]>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keryx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"middleware/",
|
|
50
50
|
"servers/",
|
|
51
51
|
"templates/",
|
|
52
|
+
"testing/",
|
|
52
53
|
"util/",
|
|
53
54
|
"api.ts",
|
|
54
55
|
"index.ts",
|
|
@@ -69,7 +70,8 @@
|
|
|
69
70
|
"./util/*": "./util/*",
|
|
70
71
|
"./config": "./config/index.ts",
|
|
71
72
|
"./config/*": "./config/*",
|
|
72
|
-
"./servers/*": "./servers/*"
|
|
73
|
+
"./servers/*": "./servers/*",
|
|
74
|
+
"./testing": "./testing/index.ts"
|
|
73
75
|
},
|
|
74
76
|
"scripts": {
|
|
75
77
|
"start": "bun keryx.ts start",
|
|
@@ -3,7 +3,7 @@ import type { ActionMiddleware } from "keryx/classes/Action.ts";
|
|
|
3
3
|
|
|
4
4
|
export const SessionMiddleware: ActionMiddleware = {
|
|
5
5
|
runBefore: async (_params, connection: Connection<{ userId?: number }>) => {
|
|
6
|
-
if (!connection.session
|
|
6
|
+
if (!connection.session?.data.userId) {
|
|
7
7
|
throw new TypedError({
|
|
8
8
|
message: "Session not found",
|
|
9
9
|
type: ErrorType.CONNECTION_SESSION_NOT_FOUND,
|
package/testing/index.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { afterAll, beforeAll } from "bun:test";
|
|
2
|
+
import { api } from "../api";
|
|
3
|
+
import type { WebServer } from "../servers/web";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generous lifecycle hook timeout (15s) for `beforeAll` / `afterAll`.
|
|
7
|
+
*
|
|
8
|
+
* `api.start()` and `api.stop()` connect to Redis, Postgres, run migrations,
|
|
9
|
+
* etc. — slower than a unit test, especially in CI. Pass this as the second
|
|
10
|
+
* argument to `beforeAll` / `afterAll` so hooks don't time out.
|
|
11
|
+
*
|
|
12
|
+
* Note: `bun:test`'s `setDefaultTimeout` and `bunfig.toml [test].timeout` only
|
|
13
|
+
* apply to `test()` blocks, not lifecycle hooks.
|
|
14
|
+
*/
|
|
15
|
+
export const HOOK_TIMEOUT = 15_000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Return the actual URL the web server bound to (with resolved port).
|
|
19
|
+
* Returns an empty string if the web server isn't running.
|
|
20
|
+
*
|
|
21
|
+
* Call after `api.start()` so the server has bound its port. Useful when
|
|
22
|
+
* `WEB_SERVER_PORT=0` is set so each test file gets a random available port.
|
|
23
|
+
*/
|
|
24
|
+
export function serverUrl(): string {
|
|
25
|
+
const web = api.servers.servers.find(
|
|
26
|
+
(s: { name: string }) => s.name === "web",
|
|
27
|
+
) as WebServer | undefined;
|
|
28
|
+
return web?.url || "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register `beforeAll` / `afterAll` hooks that start and stop the API around
|
|
33
|
+
* a test file. Returns a getter for the bound server URL — the URL isn't known
|
|
34
|
+
* until after `api.start()` binds the port, so it can't be returned directly.
|
|
35
|
+
*
|
|
36
|
+
* **Hook ordering gotcha.** `bun:test` runs `beforeAll` and `afterAll` hooks in
|
|
37
|
+
* registration order (not LIFO). So whichever hook is registered first runs
|
|
38
|
+
* first — in both phases. This matters in two ways:
|
|
39
|
+
*
|
|
40
|
+
* 1. **Config overrides that must happen before `api.start()`** — register a
|
|
41
|
+
* separate `beforeAll` that sets the config *before* calling `useTestServer()`,
|
|
42
|
+
* so it runs first.
|
|
43
|
+
* 2. **Teardown cleanup that needs Redis/DB access** — the helper's `afterAll`
|
|
44
|
+
* calls `api.stop()`, which closes the Redis and Postgres connections. Any
|
|
45
|
+
* cleanup that touches those must run *before* `api.stop()`. Since `afterAll`
|
|
46
|
+
* also runs in registration order, the helper's `api.stop()` runs first. Put
|
|
47
|
+
* connection-dependent cleanup in the same `afterAll` block as the original
|
|
48
|
+
* start/stop pair rather than splitting it, or handle cleanup in `afterEach`.
|
|
49
|
+
*
|
|
50
|
+
* @param opts.clearDatabase - Truncate all tables in `beforeAll`. Default `false`.
|
|
51
|
+
* Requires the `db` initializer to be active. Opt in for tests that mutate
|
|
52
|
+
* persistent state.
|
|
53
|
+
* @param opts.clearRedis - Flush the current Redis DB in `beforeAll`. Default
|
|
54
|
+
* `false`. Requires the `redis` initializer to be active. Opt in for tests
|
|
55
|
+
* that exercise pub/sub so messages from prior tests don't leak in.
|
|
56
|
+
* @returns A getter function that returns the server URL once `api.start()` has
|
|
57
|
+
* bound its port. Call the getter at each fetch site: `fetch(getUrl() + "/api/...")`.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* const getUrl = useTestServer({ clearDatabase: true });
|
|
61
|
+
*
|
|
62
|
+
* test("creates a user", async () => {
|
|
63
|
+
* const res = await fetch(getUrl() + "/api/user", { method: "PUT", ... });
|
|
64
|
+
* expect(res.status).toBe(200);
|
|
65
|
+
* });
|
|
66
|
+
*/
|
|
67
|
+
export function useTestServer(
|
|
68
|
+
opts: { clearDatabase?: boolean; clearRedis?: boolean } = {},
|
|
69
|
+
): () => string {
|
|
70
|
+
const { clearDatabase = false, clearRedis = false } = opts;
|
|
71
|
+
let url = "";
|
|
72
|
+
beforeAll(async () => {
|
|
73
|
+
await api.start();
|
|
74
|
+
url = serverUrl();
|
|
75
|
+
if (clearDatabase) await api.db.clearDatabase();
|
|
76
|
+
if (clearRedis) await api.redis.redis.flushdb();
|
|
77
|
+
}, HOOK_TIMEOUT);
|
|
78
|
+
afterAll(async () => {
|
|
79
|
+
await api.stop();
|
|
80
|
+
}, HOOK_TIMEOUT);
|
|
81
|
+
return () => url;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Poll a condition until it returns true, or throw after a timeout.
|
|
86
|
+
*
|
|
87
|
+
* Use this instead of fixed `Bun.sleep()` calls when waiting for async side
|
|
88
|
+
* effects like background tasks, pub/sub delivery, or presence updates.
|
|
89
|
+
*
|
|
90
|
+
* @param condition - Function returning a boolean (or Promise of one). Polled
|
|
91
|
+
* repeatedly until it returns truthy.
|
|
92
|
+
* @param opts.interval - Milliseconds between polls. Default 50.
|
|
93
|
+
* @param opts.timeout - Milliseconds before giving up. Default 5000.
|
|
94
|
+
* @throws {Error} If the condition doesn't become truthy before the timeout.
|
|
95
|
+
*/
|
|
96
|
+
export async function waitFor(
|
|
97
|
+
condition: () => Promise<boolean> | boolean,
|
|
98
|
+
{ interval = 50, timeout = 5000 } = {},
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
const start = Date.now();
|
|
101
|
+
while (!(await condition())) {
|
|
102
|
+
if (Date.now() - start > timeout) throw new Error("waitFor timed out");
|
|
103
|
+
await Bun.sleep(interval);
|
|
104
|
+
}
|
|
105
|
+
}
|
package/util/webRouting.ts
CHANGED
|
@@ -7,6 +7,10 @@ import { config } from "../config";
|
|
|
7
7
|
/**
|
|
8
8
|
* Match a URL path + HTTP method against registered action routes.
|
|
9
9
|
* Returns the action name and any extracted path parameters, or `null` if no match.
|
|
10
|
+
*
|
|
11
|
+
* Delegates to the pre-compiled `api.actions.router` for O(1) static-route lookup
|
|
12
|
+
* and an ordered scan of parameterized routes. This function only handles the
|
|
13
|
+
* transport concern of stripping the configured API prefix before routing.
|
|
10
14
|
*/
|
|
11
15
|
export async function determineActionName(
|
|
12
16
|
url: ReturnType<typeof parse>,
|
|
@@ -20,46 +24,14 @@ export async function determineActionName(
|
|
|
20
24
|
"",
|
|
21
25
|
);
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
if (!action?.web?.route) continue;
|
|
25
|
-
|
|
26
|
-
// Convert route with path parameters to regex
|
|
27
|
-
const routeWithParams = `${action.web.route}`.replace(/:\w+/g, "([^/]+)");
|
|
28
|
-
const matcher =
|
|
29
|
-
action.web.route instanceof RegExp
|
|
30
|
-
? action.web.route
|
|
31
|
-
: new RegExp(`^${routeWithParams}$`);
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
pathToMatch &&
|
|
35
|
-
pathToMatch.match(matcher) &&
|
|
36
|
-
method.toUpperCase() === action.web.method
|
|
37
|
-
) {
|
|
38
|
-
// Extract path parameters if the route has them
|
|
39
|
-
const pathParams: Record<string, string> = {};
|
|
40
|
-
const paramNames = (`${action.web.route}`.match(/:\w+/g) || []).map(
|
|
41
|
-
(name) => name.slice(1),
|
|
42
|
-
);
|
|
43
|
-
const match = pathToMatch.match(matcher);
|
|
27
|
+
if (!pathToMatch) return { actionName: null, pathParams: null };
|
|
44
28
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
actionName: action.name,
|
|
57
|
-
pathParams: Object.keys(pathParams).length > 0 ? pathParams : undefined,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return { actionName: null, pathParams: null };
|
|
29
|
+
const match = api.actions.router.match(
|
|
30
|
+
pathToMatch,
|
|
31
|
+
method.toUpperCase() as HTTP_METHOD,
|
|
32
|
+
);
|
|
33
|
+
if (!match) return { actionName: null, pathParams: null };
|
|
34
|
+
return { actionName: match.actionName, pathParams: match.pathParams };
|
|
63
35
|
}
|
|
64
36
|
|
|
65
37
|
/**
|