@pikku/core 0.9.5 → 0.9.6
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/CHANGELOG.md +9 -0
- package/dist/function/function-runner.d.ts +2 -2
- package/dist/function/function-runner.js +3 -3
- package/dist/function/functions.types.d.ts +4 -0
- package/dist/function/functions.types.js +6 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/middleware-runner.d.ts +5 -4
- package/dist/middleware-runner.js +20 -6
- package/dist/permissions.d.ts +3 -3
- package/dist/permissions.js +30 -8
- package/dist/pikku-state.d.ts +2 -4
- package/dist/pikku-state.js +3 -2
- package/dist/schema.js +8 -6
- package/dist/types/core.types.d.ts +4 -0
- package/dist/types/core.types.js +6 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +17 -0
- package/dist/wirings/channel/channel-handler.js +2 -1
- package/dist/wirings/channel/channel-runner.d.ts +2 -0
- package/dist/wirings/channel/channel-runner.js +23 -25
- package/dist/wirings/channel/local/local-channel-runner.js +5 -3
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +4 -2
- package/dist/wirings/http/http-runner.js +36 -53
- package/dist/wirings/http/http.types.d.ts +1 -1
- package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
- package/dist/wirings/http/routers/http-router.d.ts +12 -0
- package/dist/wirings/http/routers/http-router.js +2 -0
- package/dist/wirings/http/routers/path-to-regex.d.ts +10 -0
- package/dist/wirings/http/routers/path-to-regex.js +118 -0
- package/dist/wirings/mcp/mcp-runner.d.ts +1 -1
- package/dist/wirings/mcp/mcp-runner.js +3 -2
- package/dist/wirings/queue/queue-runner.js +3 -2
- package/dist/wirings/rpc/rpc-runner.js +2 -1
- package/dist/wirings/scheduler/scheduler-runner.d.ts +1 -1
- package/dist/wirings/scheduler/scheduler-runner.js +3 -2
- package/package.json +1 -1
- package/src/factory-functions.test.ts +37 -0
- package/src/function/function-runner.test.ts +101 -52
- package/src/function/function-runner.ts +5 -2
- package/src/function/functions.types.ts +13 -0
- package/src/index.ts +3 -11
- package/src/middleware-runner.test.ts +71 -43
- package/src/middleware-runner.ts +43 -17
- package/src/permissions.test.ts +21 -17
- package/src/permissions.ts +68 -26
- package/src/pikku-state.ts +5 -3
- package/src/schema.ts +8 -6
- package/src/types/core.types.ts +12 -0
- package/src/utils.ts +19 -0
- package/src/wirings/channel/channel-handler.ts +17 -11
- package/src/wirings/channel/channel-runner.ts +31 -25
- package/src/wirings/channel/local/local-channel-runner.test.ts +4 -0
- package/src/wirings/channel/local/local-channel-runner.ts +5 -4
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +13 -11
- package/src/wirings/http/http-runner.test.ts +18 -6
- package/src/wirings/http/http-runner.ts +55 -74
- package/src/wirings/http/http.types.ts +1 -1
- package/src/wirings/http/pikku-fetch-http-request.ts +3 -0
- package/src/wirings/http/routers/http-router.ts +15 -0
- package/src/wirings/http/routers/path-to-regex.test.ts +309 -0
- package/src/wirings/http/routers/path-to-regex.ts +162 -0
- package/src/wirings/mcp/mcp-runner.ts +18 -12
- package/src/wirings/queue/queue-runner.ts +15 -7
- package/src/wirings/rpc/rpc-runner.ts +21 -16
- package/src/wirings/scheduler/scheduler-runner.ts +18 -12
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/pikku-function.d.ts +0 -1
- package/dist/pikku-function.js +0 -1
- package/src/pikku-function.ts +0 -1
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
CoreUserSession,
|
|
13
13
|
CorePikkuMiddleware,
|
|
14
14
|
SessionServices,
|
|
15
|
+
PikkuWiringTypes,
|
|
15
16
|
} from '../../types/core.types.js'
|
|
16
|
-
import { match } from 'path-to-regexp'
|
|
17
17
|
import { MissingSessionError, NotFoundError } from '../../errors/errors.js'
|
|
18
18
|
import {
|
|
19
19
|
closeSessionServices,
|
|
@@ -32,6 +32,7 @@ import { PikkuFetchHTTPRequest } from './pikku-fetch-http-request.js'
|
|
|
32
32
|
import { PikkuChannel } from '../channel/channel.types.js'
|
|
33
33
|
import { addFunction, runPikkuFunc } from '../../function/function-runner.js'
|
|
34
34
|
import { rpcService } from '../rpc/rpc-runner.js'
|
|
35
|
+
import { httpRouter } from './routers/http-router.js'
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Registers middleware either globally or for a specific route.
|
|
@@ -49,32 +50,20 @@ export const addHTTPMiddleware = <PikkuMiddleware extends CorePikkuMiddleware>(
|
|
|
49
50
|
middleware?: PikkuMiddleware[]
|
|
50
51
|
) => {
|
|
51
52
|
const middlewareStore = pikkuState('http', 'middleware')
|
|
53
|
+
let route = '*'
|
|
52
54
|
|
|
53
|
-
if (typeof routeOrMiddleware === 'string'
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Remove existing entry for this route if it exists
|
|
57
|
-
const existingIndex = middlewareStore.findIndex(
|
|
58
|
-
(item) => item.route === route
|
|
59
|
-
)
|
|
60
|
-
if (existingIndex !== -1) {
|
|
61
|
-
middlewareStore.splice(existingIndex, 1)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
middlewareStore.push({ route, middleware })
|
|
55
|
+
if (typeof routeOrMiddleware === 'string') {
|
|
56
|
+
route = routeOrMiddleware
|
|
57
|
+
middleware = middleware!
|
|
65
58
|
} else {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
(item) => item.route === '*'
|
|
69
|
-
)
|
|
70
|
-
if (existingIndex !== -1) {
|
|
71
|
-
middlewareStore.splice(existingIndex, 1)
|
|
72
|
-
}
|
|
59
|
+
middleware = routeOrMiddleware
|
|
60
|
+
}
|
|
73
61
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
const currentMiddleware = middlewareStore.get(route)
|
|
63
|
+
if (currentMiddleware) {
|
|
64
|
+
middlewareStore.set(route, [...currentMiddleware, ...middleware])
|
|
65
|
+
} else {
|
|
66
|
+
middlewareStore.set(route, middleware)
|
|
78
67
|
}
|
|
79
68
|
}
|
|
80
69
|
|
|
@@ -113,10 +102,7 @@ export const wireHTTP = <
|
|
|
113
102
|
>
|
|
114
103
|
) => {
|
|
115
104
|
const httpMeta = pikkuState('http', 'meta')
|
|
116
|
-
const routeMeta = httpMeta.
|
|
117
|
-
(meta) =>
|
|
118
|
-
meta.route === httpWiring.route && meta.method === httpWiring.method
|
|
119
|
-
)
|
|
105
|
+
const routeMeta = httpMeta[httpWiring.method][httpWiring.route]
|
|
120
106
|
if (!routeMeta) {
|
|
121
107
|
throw new Error('Route metadata not found')
|
|
122
108
|
}
|
|
@@ -142,43 +128,29 @@ export const wireHTTP = <
|
|
|
142
128
|
* @returns {Object | undefined} An object with matched route details or undefined if no match.
|
|
143
129
|
*/
|
|
144
130
|
const getMatchingRoute = (requestType: string, requestPath: string) => {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return undefined
|
|
150
|
-
}
|
|
151
|
-
for (const route of routes.values()) {
|
|
152
|
-
// Generate a matching function from the route pattern
|
|
153
|
-
const matchFunc = match(`/${route.route}`.replace(/^\/\//, '/'), {
|
|
154
|
-
decode: decodeURIComponent,
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
// Attempt to match the request path
|
|
158
|
-
const matchedPath = matchFunc(requestPath.replace(/^\/\//, '/'))
|
|
159
|
-
|
|
160
|
-
if (matchedPath) {
|
|
161
|
-
// Aggregate global and route-specific middleware
|
|
162
|
-
const globalMiddleware = middleware
|
|
163
|
-
.filter((m) => m.route === '*' || new RegExp(m.route).test(route.route))
|
|
164
|
-
.map((m) => m.middleware)
|
|
165
|
-
.flat()
|
|
166
|
-
|
|
167
|
-
const meta = pikkuState('http', 'meta').find(
|
|
168
|
-
(meta) => meta.route === route.route && meta.method === route.method
|
|
169
|
-
)
|
|
131
|
+
const matchedPath = httpRouter.match(
|
|
132
|
+
requestType.toLowerCase() as HTTPMethod,
|
|
133
|
+
requestPath
|
|
134
|
+
)
|
|
170
135
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
136
|
+
if (matchedPath) {
|
|
137
|
+
const route = pikkuState('http', 'routes')
|
|
138
|
+
.get(requestType.toLowerCase() as HTTPMethod)!
|
|
139
|
+
.get(matchedPath.route)!
|
|
140
|
+
const meta = pikkuState('http', 'meta')[
|
|
141
|
+
requestType.toLowerCase() as PikkuWiringTypes
|
|
142
|
+
][route.route]
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
matchedPath,
|
|
146
|
+
params: matchedPath.params,
|
|
147
|
+
route,
|
|
148
|
+
permissions: route.permissions,
|
|
149
|
+
httpMiddleware: matchedPath.middleware,
|
|
150
|
+
middleware: route.middleware,
|
|
151
|
+
meta: meta!,
|
|
179
152
|
}
|
|
180
153
|
}
|
|
181
|
-
return undefined
|
|
182
154
|
}
|
|
183
155
|
|
|
184
156
|
/**
|
|
@@ -251,7 +223,8 @@ const executeRouteWithMiddleware = async (
|
|
|
251
223
|
matchedPath: any
|
|
252
224
|
params: any
|
|
253
225
|
route: CoreHTTPFunctionWiring<any, any, any>
|
|
254
|
-
|
|
226
|
+
httpMiddleware: CorePikkuMiddleware[] | undefined
|
|
227
|
+
middleware: CorePikkuMiddleware[] | undefined
|
|
255
228
|
meta: HTTPWiringMeta
|
|
256
229
|
},
|
|
257
230
|
http: PikkuHTTP,
|
|
@@ -259,7 +232,8 @@ const executeRouteWithMiddleware = async (
|
|
|
259
232
|
coerceDataFromSchema: boolean
|
|
260
233
|
}
|
|
261
234
|
) => {
|
|
262
|
-
const { matchedPath, params, route, middleware, meta } =
|
|
235
|
+
const { matchedPath, params, route, httpMiddleware, middleware, meta } =
|
|
236
|
+
matchedRoute
|
|
263
237
|
const {
|
|
264
238
|
singletonServices,
|
|
265
239
|
userSession,
|
|
@@ -347,14 +321,19 @@ const executeRouteWithMiddleware = async (
|
|
|
347
321
|
})
|
|
348
322
|
}
|
|
349
323
|
|
|
350
|
-
result = await runPikkuFunc(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
324
|
+
result = await runPikkuFunc(
|
|
325
|
+
PikkuWiringTypes.http,
|
|
326
|
+
`${meta.method}:${meta.route}`,
|
|
327
|
+
meta.pikkuFuncName,
|
|
328
|
+
{
|
|
329
|
+
getAllServices,
|
|
330
|
+
session,
|
|
331
|
+
data,
|
|
332
|
+
permissions: route.permissions,
|
|
333
|
+
coerceDataFromSchema: options.coerceDataFromSchema,
|
|
334
|
+
tags: route.tags,
|
|
335
|
+
}
|
|
336
|
+
)
|
|
358
337
|
|
|
359
338
|
// Respond with either a binary or JSON response based on configuration
|
|
360
339
|
if (route.returnsJSON === false) {
|
|
@@ -375,7 +354,8 @@ const executeRouteWithMiddleware = async (
|
|
|
375
354
|
await runMiddleware(
|
|
376
355
|
{ ...singletonServices, userSession },
|
|
377
356
|
{ http },
|
|
378
|
-
combineMiddleware({
|
|
357
|
+
combineMiddleware(PikkuWiringTypes.http, `${meta.method}:${meta.route}`, {
|
|
358
|
+
httpMiddleware,
|
|
379
359
|
wiringMiddleware: middleware,
|
|
380
360
|
wiringTags: route.tags,
|
|
381
361
|
funcMiddleware: funcConfig?.middleware,
|
|
@@ -506,7 +486,8 @@ export const fetchData = async <In, Out>(
|
|
|
506
486
|
ignoreMiddleware
|
|
507
487
|
? {
|
|
508
488
|
...matchedRoute,
|
|
509
|
-
middleware:
|
|
489
|
+
middleware: undefined,
|
|
490
|
+
httpMiddleware: undefined,
|
|
510
491
|
}
|
|
511
492
|
: matchedRoute,
|
|
512
493
|
http!,
|
|
@@ -208,7 +208,7 @@ export type HTTPWiringMeta = {
|
|
|
208
208
|
tags?: string[]
|
|
209
209
|
sse?: true
|
|
210
210
|
}
|
|
211
|
-
export type HTTPWiringsMeta =
|
|
211
|
+
export type HTTPWiringsMeta = Record<HTTPMethod, Record<string, HTTPWiringMeta>>
|
|
212
212
|
|
|
213
213
|
export type HTTPFunctionsMeta = Array<{
|
|
214
214
|
name: string
|
|
@@ -57,6 +57,9 @@ export class PikkuFetchHTTPRequest<In = unknown>
|
|
|
57
57
|
* @returns An object containing the cookies.
|
|
58
58
|
*/
|
|
59
59
|
public cookie(cookieName: string): string | null {
|
|
60
|
+
if (this.#cookies?.[cookieName]) {
|
|
61
|
+
return this.#cookies[cookieName]
|
|
62
|
+
}
|
|
60
63
|
const cookieHeader = this.header('cookie')
|
|
61
64
|
this.#cookies = cookieHeader ? parseCookie(cookieHeader) : {}
|
|
62
65
|
return this.#cookies[cookieName] || null
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CorePikkuMiddleware } from '../../../types/core.types.js'
|
|
2
|
+
import { HTTPMethod } from '../http.types.js'
|
|
3
|
+
import { PathToRegexRouter } from './path-to-regex.js'
|
|
4
|
+
|
|
5
|
+
export type MatchResult = {
|
|
6
|
+
route: string
|
|
7
|
+
params: Record<string, any>
|
|
8
|
+
middleware?: CorePikkuMiddleware[]
|
|
9
|
+
} | null
|
|
10
|
+
|
|
11
|
+
export interface Router {
|
|
12
|
+
match(method: HTTPMethod, path: string): MatchResult
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const httpRouter = new PathToRegexRouter()
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { test, describe, beforeEach } from 'node:test'
|
|
2
|
+
import * as assert from 'node:assert/strict'
|
|
3
|
+
import { PathToRegexRouter } from './path-to-regex.js'
|
|
4
|
+
import { resetPikkuState, pikkuState } from '../../../pikku-state.js'
|
|
5
|
+
import { HTTPMethod } from '../http.types.js'
|
|
6
|
+
|
|
7
|
+
describe('PathToRegexRouter', () => {
|
|
8
|
+
let router: PathToRegexRouter
|
|
9
|
+
|
|
10
|
+
// Helper to create mock route wiring
|
|
11
|
+
const mockRoute = (route: string) => ({
|
|
12
|
+
route,
|
|
13
|
+
method: 'get' as HTTPMethod,
|
|
14
|
+
func: (() => {}) as any,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
resetPikkuState()
|
|
19
|
+
router = new PathToRegexRouter()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('Static Route Optimization', () => {
|
|
23
|
+
test('should correctly identify static routes', () => {
|
|
24
|
+
// Setup test routes
|
|
25
|
+
pikkuState(
|
|
26
|
+
'http',
|
|
27
|
+
'routes',
|
|
28
|
+
new Map([
|
|
29
|
+
[
|
|
30
|
+
'get',
|
|
31
|
+
new Map([
|
|
32
|
+
['/static', mockRoute('/static')],
|
|
33
|
+
['/api/about', mockRoute('/api/about')],
|
|
34
|
+
['/health', mockRoute('/health')],
|
|
35
|
+
]),
|
|
36
|
+
],
|
|
37
|
+
])
|
|
38
|
+
)
|
|
39
|
+
pikkuState('http', 'middleware', new Map())
|
|
40
|
+
pikkuState('channel', 'channels', new Map())
|
|
41
|
+
|
|
42
|
+
router.initialize()
|
|
43
|
+
|
|
44
|
+
// Test static routes - should return exact matches with empty params
|
|
45
|
+
const staticResult = router.match('get', '/static')
|
|
46
|
+
assert.ok(staticResult, 'Static route should match')
|
|
47
|
+
assert.strictEqual(staticResult.route, '/static')
|
|
48
|
+
assert.deepStrictEqual(staticResult.params, {})
|
|
49
|
+
assert.strictEqual(staticResult.middleware?.length, 0)
|
|
50
|
+
|
|
51
|
+
const aboutResult = router.match('get', '/api/about')
|
|
52
|
+
assert.ok(aboutResult, 'Static route /api/about should match')
|
|
53
|
+
assert.strictEqual(aboutResult.route, '/api/about')
|
|
54
|
+
assert.deepStrictEqual(aboutResult.params, {})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('should handle dynamic routes with parameters', () => {
|
|
58
|
+
// Setup test routes with parameters
|
|
59
|
+
pikkuState(
|
|
60
|
+
'http',
|
|
61
|
+
'routes',
|
|
62
|
+
new Map([
|
|
63
|
+
[
|
|
64
|
+
'get',
|
|
65
|
+
new Map([
|
|
66
|
+
['/api/users/:id', mockRoute('/api/users/:id')],
|
|
67
|
+
[
|
|
68
|
+
'/posts/:slug/comments/:commentId',
|
|
69
|
+
mockRoute('/posts/:slug/comments/:commentId'),
|
|
70
|
+
],
|
|
71
|
+
]),
|
|
72
|
+
],
|
|
73
|
+
])
|
|
74
|
+
)
|
|
75
|
+
pikkuState('http', 'middleware', new Map())
|
|
76
|
+
pikkuState('channel', 'channels', new Map())
|
|
77
|
+
|
|
78
|
+
router.initialize()
|
|
79
|
+
|
|
80
|
+
// Test dynamic routes - should return matches with extracted parameters
|
|
81
|
+
const userResult = router.match('get', '/api/users/123')
|
|
82
|
+
assert.ok(userResult, 'Dynamic route should match')
|
|
83
|
+
assert.strictEqual(userResult.route, '/api/users/:id')
|
|
84
|
+
assert.strictEqual(userResult.params.id, '123')
|
|
85
|
+
|
|
86
|
+
const commentResult = router.match(
|
|
87
|
+
'get',
|
|
88
|
+
'/posts/hello-world/comments/456'
|
|
89
|
+
)
|
|
90
|
+
assert.ok(commentResult, 'Complex dynamic route should match')
|
|
91
|
+
assert.strictEqual(
|
|
92
|
+
commentResult.route,
|
|
93
|
+
'/posts/:slug/comments/:commentId'
|
|
94
|
+
)
|
|
95
|
+
assert.strictEqual(commentResult.params.slug, 'hello-world')
|
|
96
|
+
assert.strictEqual(commentResult.params.commentId, '456')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('should handle multi-parameter routes', () => {
|
|
100
|
+
// Setup routes with multiple parameters
|
|
101
|
+
pikkuState(
|
|
102
|
+
'http',
|
|
103
|
+
'routes',
|
|
104
|
+
new Map([
|
|
105
|
+
[
|
|
106
|
+
'get',
|
|
107
|
+
new Map([
|
|
108
|
+
[
|
|
109
|
+
'/api/:version/users/:userId',
|
|
110
|
+
mockRoute('/api/:version/users/:userId'),
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
'/blog/:year/:month/:slug',
|
|
114
|
+
mockRoute('/blog/:year/:month/:slug'),
|
|
115
|
+
],
|
|
116
|
+
]),
|
|
117
|
+
],
|
|
118
|
+
])
|
|
119
|
+
)
|
|
120
|
+
pikkuState('http', 'middleware', new Map())
|
|
121
|
+
pikkuState('channel', 'channels', new Map())
|
|
122
|
+
|
|
123
|
+
router.initialize()
|
|
124
|
+
|
|
125
|
+
// Test multi-parameter routes
|
|
126
|
+
const apiResult = router.match('get', '/api/v2/users/456')
|
|
127
|
+
assert.ok(apiResult, 'Multi-parameter route should match')
|
|
128
|
+
assert.strictEqual(apiResult.route, '/api/:version/users/:userId')
|
|
129
|
+
assert.strictEqual(apiResult.params.version, 'v2')
|
|
130
|
+
assert.strictEqual(apiResult.params.userId, '456')
|
|
131
|
+
|
|
132
|
+
const blogResult = router.match('get', '/blog/2024/03/hello-world')
|
|
133
|
+
assert.ok(blogResult, 'Deep multi-parameter route should match')
|
|
134
|
+
assert.strictEqual(blogResult.route, '/blog/:year/:month/:slug')
|
|
135
|
+
assert.strictEqual(blogResult.params.year, '2024')
|
|
136
|
+
assert.strictEqual(blogResult.params.month, '03')
|
|
137
|
+
assert.strictEqual(blogResult.params.slug, 'hello-world')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('should prioritize static routes over dynamic routes for performance', () => {
|
|
141
|
+
// Setup mixed routes where static could conflict with dynamic
|
|
142
|
+
pikkuState(
|
|
143
|
+
'http',
|
|
144
|
+
'routes',
|
|
145
|
+
new Map([
|
|
146
|
+
[
|
|
147
|
+
'get',
|
|
148
|
+
new Map([
|
|
149
|
+
['/api/users', mockRoute('/api/users')], // static
|
|
150
|
+
['/api/:resource', mockRoute('/api/:resource')], // dynamic that could match above
|
|
151
|
+
]),
|
|
152
|
+
],
|
|
153
|
+
])
|
|
154
|
+
)
|
|
155
|
+
pikkuState('http', 'middleware', new Map())
|
|
156
|
+
pikkuState('channel', 'channels', new Map())
|
|
157
|
+
|
|
158
|
+
router.initialize()
|
|
159
|
+
|
|
160
|
+
// Static route should be found first (O(1) lookup)
|
|
161
|
+
const result = router.match('get', '/api/users')
|
|
162
|
+
assert.ok(result, 'Route should match')
|
|
163
|
+
assert.strictEqual(result.route, '/api/users') // Should match static, not dynamic
|
|
164
|
+
assert.deepStrictEqual(result.params, {})
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('Path Normalization', () => {
|
|
169
|
+
test('should normalize paths without leading slash', () => {
|
|
170
|
+
pikkuState(
|
|
171
|
+
'http',
|
|
172
|
+
'routes',
|
|
173
|
+
new Map([
|
|
174
|
+
[
|
|
175
|
+
'get',
|
|
176
|
+
new Map([
|
|
177
|
+
['test', mockRoute('test')], // route without leading slash
|
|
178
|
+
['api/health', mockRoute('api/health')],
|
|
179
|
+
]),
|
|
180
|
+
],
|
|
181
|
+
])
|
|
182
|
+
)
|
|
183
|
+
pikkuState('http', 'middleware', new Map())
|
|
184
|
+
pikkuState('channel', 'channels', new Map())
|
|
185
|
+
|
|
186
|
+
router.initialize()
|
|
187
|
+
|
|
188
|
+
// Should match with or without leading slash in request
|
|
189
|
+
const result1 = router.match('get', '/test')
|
|
190
|
+
assert.ok(result1, 'Should match /test')
|
|
191
|
+
assert.strictEqual(result1.route, 'test')
|
|
192
|
+
|
|
193
|
+
const result2 = router.match('get', 'test')
|
|
194
|
+
assert.ok(result2, 'Should match test')
|
|
195
|
+
assert.strictEqual(result2.route, 'test')
|
|
196
|
+
|
|
197
|
+
const result3 = router.match('get', '/api/health')
|
|
198
|
+
assert.ok(result3, 'Should match /api/health')
|
|
199
|
+
assert.strictEqual(result3.route, 'api/health')
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe('Channel Routes Integration', () => {
|
|
204
|
+
test('should handle channel routes as GET routes', () => {
|
|
205
|
+
pikkuState('http', 'routes', new Map())
|
|
206
|
+
pikkuState('http', 'middleware', new Map())
|
|
207
|
+
pikkuState(
|
|
208
|
+
'channel',
|
|
209
|
+
'channels',
|
|
210
|
+
new Map([
|
|
211
|
+
['websocket-test', { name: 'websocket-test', route: '/ws/test' }],
|
|
212
|
+
['chat', { name: 'chat', route: '/chat/:room' }],
|
|
213
|
+
])
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
router.initialize()
|
|
217
|
+
|
|
218
|
+
// Channel routes should be accessible via GET method
|
|
219
|
+
const wsResult = router.match('get', '/ws/test')
|
|
220
|
+
assert.ok(wsResult, 'Static channel route should match')
|
|
221
|
+
assert.strictEqual(wsResult.route, '/ws/test')
|
|
222
|
+
assert.deepStrictEqual(wsResult.params, {})
|
|
223
|
+
|
|
224
|
+
const chatResult = router.match('get', '/chat/general')
|
|
225
|
+
assert.ok(chatResult, 'Dynamic channel route should match')
|
|
226
|
+
assert.strictEqual(chatResult.route, '/chat/:room')
|
|
227
|
+
assert.strictEqual(chatResult.params.room, 'general')
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('Middleware Integration', () => {
|
|
232
|
+
test('should include global and route-specific middleware', () => {
|
|
233
|
+
const globalMiddleware = [async () => {}]
|
|
234
|
+
const routeMiddleware = [async () => {}]
|
|
235
|
+
|
|
236
|
+
pikkuState(
|
|
237
|
+
'http',
|
|
238
|
+
'routes',
|
|
239
|
+
new Map([['get', new Map([['/api/test', mockRoute('/api/test')]])]])
|
|
240
|
+
)
|
|
241
|
+
pikkuState(
|
|
242
|
+
'http',
|
|
243
|
+
'middleware',
|
|
244
|
+
new Map([
|
|
245
|
+
['*', globalMiddleware],
|
|
246
|
+
['/api/test', routeMiddleware],
|
|
247
|
+
])
|
|
248
|
+
)
|
|
249
|
+
pikkuState('channel', 'channels', new Map())
|
|
250
|
+
|
|
251
|
+
router.initialize()
|
|
252
|
+
|
|
253
|
+
const result = router.match('get', '/api/test')
|
|
254
|
+
assert.ok(result, 'Route should match')
|
|
255
|
+
assert.strictEqual(result.middleware?.length, 2) // global + route-specific
|
|
256
|
+
assert.strictEqual(result.middleware?.[0], globalMiddleware[0])
|
|
257
|
+
assert.strictEqual(result.middleware?.[1], routeMiddleware[0])
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('No Match Scenarios', () => {
|
|
262
|
+
test('should return null for non-existent routes', () => {
|
|
263
|
+
pikkuState(
|
|
264
|
+
'http',
|
|
265
|
+
'routes',
|
|
266
|
+
new Map([['get', new Map([['/existing', mockRoute('/existing')]])]])
|
|
267
|
+
)
|
|
268
|
+
pikkuState('http', 'middleware', new Map())
|
|
269
|
+
pikkuState('channel', 'channels', new Map())
|
|
270
|
+
|
|
271
|
+
router.initialize()
|
|
272
|
+
|
|
273
|
+
const result = router.match('get', '/non-existent')
|
|
274
|
+
assert.strictEqual(result, null)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('should return null for unsupported HTTP methods', () => {
|
|
278
|
+
pikkuState(
|
|
279
|
+
'http',
|
|
280
|
+
'routes',
|
|
281
|
+
new Map([['get', new Map([['/test', mockRoute('/test')]])]])
|
|
282
|
+
)
|
|
283
|
+
pikkuState('http', 'middleware', new Map())
|
|
284
|
+
pikkuState('channel', 'channels', new Map())
|
|
285
|
+
|
|
286
|
+
router.initialize()
|
|
287
|
+
|
|
288
|
+
const result = router.match('post', '/test')
|
|
289
|
+
assert.strictEqual(result, null)
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
describe('Lazy Initialization', () => {
|
|
294
|
+
test('should initialize automatically on first match call', () => {
|
|
295
|
+
pikkuState(
|
|
296
|
+
'http',
|
|
297
|
+
'routes',
|
|
298
|
+
new Map([['get', new Map([['/test', mockRoute('/test')]])]])
|
|
299
|
+
)
|
|
300
|
+
pikkuState('http', 'middleware', new Map())
|
|
301
|
+
pikkuState('channel', 'channels', new Map())
|
|
302
|
+
|
|
303
|
+
// Don't call initialize manually
|
|
304
|
+
const result = router.match('get', '/test')
|
|
305
|
+
assert.ok(result, 'Should auto-initialize and match')
|
|
306
|
+
assert.strictEqual(result.route, '/test')
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
})
|