@pikku/core 0.9.4 → 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 +28 -0
- package/dist/function/function-runner.d.ts +2 -2
- package/dist/function/function-runner.js +22 -25
- 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 +22 -13
- package/dist/middleware-runner.js +44 -16
- package/dist/permissions.d.ts +20 -4
- package/dist/permissions.js +84 -2
- package/dist/pikku-state.d.ts +4 -6
- package/dist/pikku-state.js +6 -5
- package/dist/schema.js +8 -6
- package/dist/types/core.types.d.ts +7 -4
- 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 +9 -4
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +8 -3
- package/dist/wirings/http/http-runner.js +44 -54
- package/dist/wirings/http/http.types.d.ts +5 -5
- 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 +10 -4
- package/dist/wirings/queue/queue-runner.js +11 -3
- package/dist/wirings/rpc/rpc-runner.d.ts +1 -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 +10 -4
- package/package.json +1 -1
- package/src/factory-functions.test.ts +37 -0
- package/src/function/function-runner.test.ts +499 -0
- package/src/function/function-runner.ts +26 -43
- package/src/function/functions.types.ts +13 -0
- package/src/index.ts +3 -7
- package/src/middleware-runner.test.ts +357 -0
- package/src/middleware-runner.ts +68 -21
- package/src/permissions.test.ts +407 -42
- package/src/permissions.ts +159 -7
- package/src/pikku-state.ts +14 -8
- package/src/schema.ts +8 -6
- package/src/types/core.types.ts +15 -4
- 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 +9 -8
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +17 -15
- package/src/wirings/http/http-runner.test.ts +18 -6
- package/src/wirings/http/http-runner.ts +64 -75
- package/src/wirings/http/http.types.ts +5 -5
- 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 +25 -14
- package/src/wirings/queue/queue-runner.ts +26 -8
- package/src/wirings/rpc/rpc-runner.ts +21 -16
- package/src/wirings/scheduler/scheduler-runner.ts +27 -14
- 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
|
@@ -83,8 +83,8 @@ export type CoreHTTPFunction = {
|
|
|
83
83
|
/**
|
|
84
84
|
* Represents a http interaction within Pikku, including a request and response.
|
|
85
85
|
*/
|
|
86
|
-
export interface PikkuHTTP {
|
|
87
|
-
request?: PikkuHTTPRequest
|
|
86
|
+
export interface PikkuHTTP<In = unknown> {
|
|
87
|
+
request?: PikkuHTTPRequest<In>
|
|
88
88
|
response?: PikkuHTTPResponse
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -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
|
|
@@ -234,7 +234,7 @@ export interface PikkuHTTPRequest<In = unknown> {
|
|
|
234
234
|
query(): PikkuQuery
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
export interface PikkuHTTPResponse {
|
|
237
|
+
export interface PikkuHTTPResponse<Out = unknown> {
|
|
238
238
|
status(code: number): this
|
|
239
239
|
cookie(name: string, value: string | null, options: SerializeOptions): this
|
|
240
240
|
header(name: string, value: string | string[]): this
|
|
@@ -248,7 +248,7 @@ export interface PikkuHTTPResponse {
|
|
|
248
248
|
| URLSearchParams
|
|
249
249
|
| ReadableStream
|
|
250
250
|
): this
|
|
251
|
-
json(data:
|
|
251
|
+
json(data: Out): this
|
|
252
252
|
redirect(location: string, status?: number): this
|
|
253
253
|
close?: () => void
|
|
254
254
|
setMode?: (mode: 'stream') => void
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { match, MatchFunction } from 'path-to-regexp'
|
|
2
|
+
import { MatchResult, Router } from './http-router.js'
|
|
3
|
+
import { HTTPMethod } from '../http.types.js'
|
|
4
|
+
import { pikkuState } from '../../../pikku-state.js'
|
|
5
|
+
import { CorePikkuMiddleware } from '../../../types/core.types.js'
|
|
6
|
+
|
|
7
|
+
interface CompiledRoute {
|
|
8
|
+
matcher: MatchFunction<Partial<Record<string, string | string[]>>>
|
|
9
|
+
route: string
|
|
10
|
+
middleware: CorePikkuMiddleware[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface StaticRoute {
|
|
14
|
+
route: string
|
|
15
|
+
middleware: CorePikkuMiddleware[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class PathToRegexRouter implements Router {
|
|
19
|
+
private compiledRoutes: Map<HTTPMethod, Map<string, CompiledRoute>> =
|
|
20
|
+
new Map()
|
|
21
|
+
private staticRoutes: Map<HTTPMethod, Map<string, StaticRoute>> = new Map()
|
|
22
|
+
private precompiledMiddleware: Map<string, CorePikkuMiddleware[]> = new Map()
|
|
23
|
+
private isInitialized = false
|
|
24
|
+
|
|
25
|
+
public initialize() {
|
|
26
|
+
const routes = pikkuState('http', 'routes')
|
|
27
|
+
const channelRoutes = pikkuState('channel', 'channels')
|
|
28
|
+
const middlewareMap = pikkuState('http', 'middleware')
|
|
29
|
+
|
|
30
|
+
// Precompile middleware lookups
|
|
31
|
+
for (const [middlewareRoute, middlewareArray] of middlewareMap.entries()) {
|
|
32
|
+
this.precompiledMiddleware.set(middlewareRoute, middlewareArray)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Helper function to compile routes for a given method
|
|
36
|
+
const compileRoutesForMethod = (
|
|
37
|
+
method: HTTPMethod,
|
|
38
|
+
routeEntries: Iterable<[string, any]>
|
|
39
|
+
) => {
|
|
40
|
+
const methodCompiledRoutes =
|
|
41
|
+
this.compiledRoutes.get(method) || new Map<string, CompiledRoute>()
|
|
42
|
+
const methodStaticRoutes =
|
|
43
|
+
this.staticRoutes.get(method) || new Map<string, StaticRoute>()
|
|
44
|
+
|
|
45
|
+
for (const [routePath] of routeEntries) {
|
|
46
|
+
// Normalize route path - ensure it starts with /
|
|
47
|
+
const normalizedRoutePath = routePath.startsWith('/')
|
|
48
|
+
? routePath
|
|
49
|
+
: `/${routePath}`
|
|
50
|
+
|
|
51
|
+
// Check if route is static (no parameters or wildcards)
|
|
52
|
+
const isStaticRoute = !/\*|:/.test(normalizedRoutePath)
|
|
53
|
+
|
|
54
|
+
// Precompute middleware for this route
|
|
55
|
+
const routeMiddleware: CorePikkuMiddleware[] = []
|
|
56
|
+
|
|
57
|
+
// Add global middleware (*)
|
|
58
|
+
const globalMiddleware = this.precompiledMiddleware.get('*')
|
|
59
|
+
if (globalMiddleware) {
|
|
60
|
+
routeMiddleware.push(...globalMiddleware)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Add route-specific middleware
|
|
64
|
+
for (const [
|
|
65
|
+
middlewareRoute,
|
|
66
|
+
middlewareArray,
|
|
67
|
+
] of this.precompiledMiddleware.entries()) {
|
|
68
|
+
if (middlewareRoute !== '*') {
|
|
69
|
+
// Use regex test for pattern matching
|
|
70
|
+
try {
|
|
71
|
+
if (new RegExp(middlewareRoute).test(normalizedRoutePath)) {
|
|
72
|
+
routeMiddleware.push(...middlewareArray)
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// If regex is invalid, do exact match
|
|
76
|
+
if (middlewareRoute === normalizedRoutePath) {
|
|
77
|
+
routeMiddleware.push(...middlewareArray)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isStaticRoute) {
|
|
84
|
+
// Store static routes for O(1) lookup
|
|
85
|
+
methodStaticRoutes.set(normalizedRoutePath, {
|
|
86
|
+
route: routePath, // Keep the original route path for lookup in pikkuState
|
|
87
|
+
middleware: routeMiddleware,
|
|
88
|
+
})
|
|
89
|
+
} else {
|
|
90
|
+
// Compile dynamic routes with path-to-regexp
|
|
91
|
+
const matcher = match(normalizedRoutePath, {
|
|
92
|
+
decode: decodeURIComponent,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
methodCompiledRoutes.set(normalizedRoutePath, {
|
|
96
|
+
matcher,
|
|
97
|
+
route: routePath, // Keep the original route path for lookup in pikkuState
|
|
98
|
+
middleware: routeMiddleware,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.compiledRoutes.set(method, methodCompiledRoutes)
|
|
104
|
+
this.staticRoutes.set(method, methodStaticRoutes)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Precompile all HTTP route matchers
|
|
108
|
+
for (const [method, routeMap] of routes.entries()) {
|
|
109
|
+
compileRoutesForMethod(method, routeMap.entries())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Precompile all channel route matchers (treating them as GET routes for WebSocket upgrades)
|
|
113
|
+
const channelRoutesArray: Array<[string, any]> = Array.from(
|
|
114
|
+
channelRoutes.entries()
|
|
115
|
+
).map(([, channel]) => [channel.route, channel])
|
|
116
|
+
compileRoutesForMethod('get', channelRoutesArray)
|
|
117
|
+
|
|
118
|
+
this.isInitialized = true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
match(method: HTTPMethod, path: string): MatchResult {
|
|
122
|
+
if (!this.isInitialized) {
|
|
123
|
+
this.initialize()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Normalize path - ensure it starts with /
|
|
127
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
|
128
|
+
|
|
129
|
+
// First, try static routes for O(1) lookup
|
|
130
|
+
const methodStaticRoutes = this.staticRoutes.get(method)
|
|
131
|
+
if (methodStaticRoutes) {
|
|
132
|
+
const staticRoute = methodStaticRoutes.get(normalizedPath)
|
|
133
|
+
if (staticRoute) {
|
|
134
|
+
return {
|
|
135
|
+
route: staticRoute.route,
|
|
136
|
+
params: {},
|
|
137
|
+
middleware: staticRoute.middleware,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// If no static route matched, try dynamic routes
|
|
143
|
+
const methodRoutes = this.compiledRoutes.get(method)
|
|
144
|
+
if (!methodRoutes) {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Try each compiled route for this method
|
|
149
|
+
for (const [, compiledRoute] of methodRoutes.entries()) {
|
|
150
|
+
const result = compiledRoute.matcher(normalizedPath)
|
|
151
|
+
if (result) {
|
|
152
|
+
return {
|
|
153
|
+
route: compiledRoute.route,
|
|
154
|
+
params: result.params,
|
|
155
|
+
middleware: compiledRoute.middleware,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {
|
|
2
|
+
PikkuWiringTypes,
|
|
3
|
+
type CoreServices,
|
|
4
|
+
type CoreSingletonServices,
|
|
5
|
+
type CoreUserSession,
|
|
6
|
+
type CreateSessionServices,
|
|
6
7
|
} from '../../types/core.types.js'
|
|
7
8
|
import type {
|
|
8
9
|
CoreMCPResource,
|
|
@@ -20,7 +21,7 @@ import { pikkuState } from '../../pikku-state.js'
|
|
|
20
21
|
import { addFunction, runPikkuFunc } from '../../function/function-runner.js'
|
|
21
22
|
import { rpcService } from '../rpc/rpc-runner.js'
|
|
22
23
|
import { BadRequestError, NotFoundError } from '../../errors/errors.js'
|
|
23
|
-
import {
|
|
24
|
+
import { combineMiddleware, runMiddleware } from '../../middleware-runner.js'
|
|
24
25
|
|
|
25
26
|
export class MCPError extends Error {
|
|
26
27
|
constructor(public readonly error: JsonRpcErrorResponse) {
|
|
@@ -253,19 +254,29 @@ async function runMCPPikkuFunc(
|
|
|
253
254
|
})
|
|
254
255
|
}
|
|
255
256
|
|
|
256
|
-
result = await runPikkuFunc(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
257
|
+
result = await runPikkuFunc(
|
|
258
|
+
PikkuWiringTypes.mcp,
|
|
259
|
+
`${type}:${name}`,
|
|
260
|
+
pikkuFuncName,
|
|
261
|
+
{
|
|
262
|
+
getAllServices,
|
|
263
|
+
session,
|
|
264
|
+
data: request.params,
|
|
265
|
+
tags: mcp.tags,
|
|
266
|
+
}
|
|
267
|
+
)
|
|
262
268
|
}
|
|
263
269
|
|
|
264
|
-
|
|
270
|
+
const funcConfig = pikkuState('function', 'functions').get(pikkuFuncName!)
|
|
265
271
|
await runMiddleware(
|
|
266
272
|
singletonServices,
|
|
267
273
|
{ mcp: interaction },
|
|
268
|
-
|
|
274
|
+
combineMiddleware(PikkuWiringTypes.mcp, `${type}:${name}`, {
|
|
275
|
+
wiringMiddleware: mcp.middleware,
|
|
276
|
+
wiringTags: mcp.tags,
|
|
277
|
+
funcMiddleware: funcConfig?.middleware,
|
|
278
|
+
funcTags: funcConfig?.tags,
|
|
279
|
+
}),
|
|
269
280
|
runMain
|
|
270
281
|
)
|
|
271
282
|
|
|
@@ -4,8 +4,11 @@ import type { CorePikkuFunctionSessionless } from '../../function/functions.type
|
|
|
4
4
|
import { getErrorResponse, PikkuError } from '../../errors/error-handler.js'
|
|
5
5
|
import { pikkuState } from '../../pikku-state.js'
|
|
6
6
|
import { addFunction, runPikkuFunc } from '../../function/function-runner.js'
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
CreateSessionServices,
|
|
9
|
+
PikkuWiringTypes,
|
|
10
|
+
} from '../../types/core.types.js'
|
|
11
|
+
import { combineMiddleware, runMiddleware } from '../../middleware-runner.js'
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Error class for queue processor not found
|
|
@@ -149,22 +152,37 @@ export async function runQueueJob({
|
|
|
149
152
|
})
|
|
150
153
|
|
|
151
154
|
// Execute the pikku function with the job data
|
|
152
|
-
result = await runPikkuFunc(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
result = await runPikkuFunc(
|
|
156
|
+
PikkuWiringTypes.queue,
|
|
157
|
+
job.queueName,
|
|
158
|
+
processorMeta.pikkuFuncName,
|
|
159
|
+
{
|
|
160
|
+
getAllServices,
|
|
161
|
+
data: job.data,
|
|
162
|
+
tags: queueWorker.tags,
|
|
163
|
+
}
|
|
164
|
+
)
|
|
157
165
|
|
|
158
166
|
logger.debug(
|
|
159
167
|
`Successfully processed job ${job.id} in queue ${job.queueName}`
|
|
160
168
|
)
|
|
161
169
|
}
|
|
162
170
|
|
|
171
|
+
// Get function config for middleware and tags
|
|
172
|
+
const funcConfig = pikkuState('function', 'functions').get(
|
|
173
|
+
processorMeta.pikkuFuncName
|
|
174
|
+
)
|
|
175
|
+
|
|
163
176
|
// Get middleware for tags and combine with queue-specific middleware
|
|
164
177
|
await runMiddleware(
|
|
165
178
|
singletonServices,
|
|
166
179
|
{ queue },
|
|
167
|
-
|
|
180
|
+
combineMiddleware(PikkuWiringTypes.queue, `${job.queueName}:${job.id}`, {
|
|
181
|
+
wiringMiddleware: queueWorker.middleware,
|
|
182
|
+
wiringTags: queueWorker.tags,
|
|
183
|
+
funcMiddleware: funcConfig?.middleware,
|
|
184
|
+
funcTags: funcConfig?.tags,
|
|
185
|
+
}),
|
|
168
186
|
runMain
|
|
169
187
|
)
|
|
170
188
|
|