@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/function/function-runner.d.ts +2 -2
  3. package/dist/function/function-runner.js +22 -25
  4. package/dist/function/functions.types.d.ts +4 -0
  5. package/dist/function/functions.types.js +6 -1
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +3 -3
  8. package/dist/middleware-runner.d.ts +22 -13
  9. package/dist/middleware-runner.js +44 -16
  10. package/dist/permissions.d.ts +20 -4
  11. package/dist/permissions.js +84 -2
  12. package/dist/pikku-state.d.ts +4 -6
  13. package/dist/pikku-state.js +6 -5
  14. package/dist/schema.js +8 -6
  15. package/dist/types/core.types.d.ts +7 -4
  16. package/dist/types/core.types.js +6 -0
  17. package/dist/utils.d.ts +1 -0
  18. package/dist/utils.js +17 -0
  19. package/dist/wirings/channel/channel-handler.js +2 -1
  20. package/dist/wirings/channel/channel-runner.d.ts +2 -0
  21. package/dist/wirings/channel/channel-runner.js +23 -25
  22. package/dist/wirings/channel/local/local-channel-runner.js +9 -4
  23. package/dist/wirings/channel/serverless/serverless-channel-runner.js +8 -3
  24. package/dist/wirings/http/http-runner.js +44 -54
  25. package/dist/wirings/http/http.types.d.ts +5 -5
  26. package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
  27. package/dist/wirings/http/routers/http-router.d.ts +12 -0
  28. package/dist/wirings/http/routers/http-router.js +2 -0
  29. package/dist/wirings/http/routers/path-to-regex.d.ts +10 -0
  30. package/dist/wirings/http/routers/path-to-regex.js +118 -0
  31. package/dist/wirings/mcp/mcp-runner.d.ts +1 -1
  32. package/dist/wirings/mcp/mcp-runner.js +10 -4
  33. package/dist/wirings/queue/queue-runner.js +11 -3
  34. package/dist/wirings/rpc/rpc-runner.d.ts +1 -2
  35. package/dist/wirings/rpc/rpc-runner.js +2 -1
  36. package/dist/wirings/scheduler/scheduler-runner.d.ts +1 -1
  37. package/dist/wirings/scheduler/scheduler-runner.js +10 -4
  38. package/package.json +1 -1
  39. package/src/factory-functions.test.ts +37 -0
  40. package/src/function/function-runner.test.ts +499 -0
  41. package/src/function/function-runner.ts +26 -43
  42. package/src/function/functions.types.ts +13 -0
  43. package/src/index.ts +3 -7
  44. package/src/middleware-runner.test.ts +357 -0
  45. package/src/middleware-runner.ts +68 -21
  46. package/src/permissions.test.ts +407 -42
  47. package/src/permissions.ts +159 -7
  48. package/src/pikku-state.ts +14 -8
  49. package/src/schema.ts +8 -6
  50. package/src/types/core.types.ts +15 -4
  51. package/src/utils.ts +19 -0
  52. package/src/wirings/channel/channel-handler.ts +17 -11
  53. package/src/wirings/channel/channel-runner.ts +31 -25
  54. package/src/wirings/channel/local/local-channel-runner.test.ts +4 -0
  55. package/src/wirings/channel/local/local-channel-runner.ts +9 -8
  56. package/src/wirings/channel/serverless/serverless-channel-runner.ts +17 -15
  57. package/src/wirings/http/http-runner.test.ts +18 -6
  58. package/src/wirings/http/http-runner.ts +64 -75
  59. package/src/wirings/http/http.types.ts +5 -5
  60. package/src/wirings/http/pikku-fetch-http-request.ts +3 -0
  61. package/src/wirings/http/routers/http-router.ts +15 -0
  62. package/src/wirings/http/routers/path-to-regex.test.ts +309 -0
  63. package/src/wirings/http/routers/path-to-regex.ts +162 -0
  64. package/src/wirings/mcp/mcp-runner.ts +25 -14
  65. package/src/wirings/queue/queue-runner.ts +26 -8
  66. package/src/wirings/rpc/rpc-runner.ts +21 -16
  67. package/src/wirings/scheduler/scheduler-runner.ts +27 -14
  68. package/tsconfig.tsbuildinfo +1 -1
  69. package/dist/pikku-function.d.ts +0 -1
  70. package/dist/pikku-function.js +0 -1
  71. package/src/pikku-function.ts +0 -1
@@ -1,4 +1,5 @@
1
1
  import { UserSessionService } from '../../services/user-session-service.js';
2
+ import { CorePikkuMiddleware } from '../../types/core.types.js';
2
3
  import { ChannelMeta, CoreChannel, RunChannelOptions, RunChannelParams } from './channel.types.js';
3
4
  /**
4
5
  * Adds a channel and registers all functions referenced in it using the
@@ -11,4 +12,5 @@ export declare const openChannel: ({ route, singletonServices, coerceDataFromSch
11
12
  openingData: unknown;
12
13
  channelConfig: CoreChannel<unknown, any>;
13
14
  meta: ChannelMeta;
15
+ httpMiddleware: CorePikkuMiddleware[] | undefined;
14
16
  }>;
@@ -2,7 +2,7 @@ import { NotFoundError } from '../../errors/errors.js';
2
2
  import { addFunction } from '../../function/function-runner.js';
3
3
  import { pikkuState } from '../../pikku-state.js';
4
4
  import { coerceTopLevelDataFromSchema, validateSchema } from '../../schema.js';
5
- import { match } from 'path-to-regexp';
5
+ import { httpRouter } from '../http/routers/http-router.js';
6
6
  /**
7
7
  * Adds a channel and registers all functions referenced in it using the
8
8
  * function names already stored in the channel metadata
@@ -65,30 +65,28 @@ export const wireChannel = (channel) => {
65
65
  pikkuState('channel', 'channels').set(channel.name, channel);
66
66
  };
67
67
  const getMatchingChannelConfig = (request) => {
68
+ const matchedPath = httpRouter.match('get', request);
69
+ if (!matchedPath) {
70
+ return null;
71
+ }
72
+ const meta = pikkuState('channel', 'meta');
73
+ const channelMeta = Object.values(meta).find((channelConfig) => channelConfig.route === matchedPath.route);
74
+ if (!channelMeta) {
75
+ return null;
76
+ }
68
77
  const channels = pikkuState('channel', 'channels');
69
- const channelsMeta = pikkuState('channel', 'meta');
70
- for (const channelConfig of channels.values()) {
71
- const cleanedRoute = channelConfig.route.replace(/^\/\//, '/');
72
- const cleanedRequest = request.replace(/^\/\//, '/');
73
- const matchFunc = match(cleanedRoute, {
74
- decode: decodeURIComponent,
75
- });
76
- const matchedPath = matchFunc(cleanedRequest);
77
- if (matchedPath) {
78
- const channelMeta = channelsMeta[channelConfig.name];
79
- if (!channelMeta) {
80
- throw new Error(`Channel ${channelConfig.name} not found in metadata`);
81
- }
82
- return {
83
- matchedPath,
84
- params: matchedPath.params,
85
- channelConfig,
86
- schemaName: channelMeta.input,
87
- meta: channelMeta,
88
- };
89
- }
78
+ const channelConfig = channels.get(channelMeta.name);
79
+ if (!channelConfig) {
80
+ return null;
90
81
  }
91
- return null;
82
+ return {
83
+ matchedPath,
84
+ params: matchedPath.params,
85
+ channelConfig,
86
+ schemaName: channelMeta.input,
87
+ meta: channelMeta,
88
+ httpMiddleware: matchedPath.middleware,
89
+ };
92
90
  };
93
91
  export const openChannel = async ({ route, singletonServices, coerceDataFromSchema = true, request, }) => {
94
92
  const matchingChannel = getMatchingChannelConfig(route);
@@ -96,7 +94,7 @@ export const openChannel = async ({ route, singletonServices, coerceDataFromSche
96
94
  singletonServices.logger.info(`Channel not found: ${route}`);
97
95
  throw new NotFoundError(`Channel not found: ${route}`);
98
96
  }
99
- const { params, channelConfig, schemaName, meta } = matchingChannel;
97
+ const { params, channelConfig, schemaName, meta, httpMiddleware } = matchingChannel;
100
98
  const requiresSession = channelConfig.auth !== false;
101
99
  request?.setParams(params);
102
100
  singletonServices.logger.info(`Matched channel: ${channelConfig.name} | route: ${channelConfig.route} | auth: ${requiresSession.toString()}`);
@@ -108,5 +106,5 @@ export const openChannel = async ({ route, singletonServices, coerceDataFromSche
108
106
  }
109
107
  await validateSchema(singletonServices.logger, singletonServices.schema, schemaName, openingData);
110
108
  }
111
- return { openingData, channelConfig, meta };
109
+ return { openingData, channelConfig, meta, httpMiddleware };
112
110
  };
@@ -3,8 +3,9 @@ import { createHTTPInteraction } from '../../http/http-runner.js';
3
3
  import { closeSessionServices } from '../../../utils.js';
4
4
  import { processMessageHandlers } from '../channel-handler.js';
5
5
  import { PikkuLocalChannelHandler } from './local-channel-handler.js';
6
+ import { PikkuWiringTypes } from '../../../types/core.types.js';
6
7
  import { handleHTTPError } from '../../../handle-error.js';
7
- import { addMiddlewareForTags, runMiddleware, } from '../../../middleware-runner.js';
8
+ import { combineMiddleware, runMiddleware } from '../../../middleware-runner.js';
8
9
  import { PikkuUserSessionService } from '../../../services/user-session-service.js';
9
10
  import { runPikkuFuncDirectly } from '../../../function/function-runner.js';
10
11
  import { rpcService } from '../../rpc/rpc-runner.js';
@@ -17,10 +18,10 @@ export const runLocalChannel = async ({ singletonServices, channelId, request, r
17
18
  http = createHTTPInteraction(request, response);
18
19
  route = http?.request?.path();
19
20
  }
20
- let openingData, channelConfig, meta;
21
+ let openingData, channelConfig, meta, httpMiddleware;
21
22
  try {
22
23
  ;
23
- ({ openingData, channelConfig, meta } = await openChannel({
24
+ ({ openingData, channelConfig, meta, httpMiddleware } = await openChannel({
24
25
  channelId,
25
26
  createSessionServices,
26
27
  respondWith404,
@@ -77,6 +78,10 @@ export const runLocalChannel = async ({ singletonServices, channelId, request, r
77
78
  await runMiddleware({
78
79
  ...singletonServices,
79
80
  userSession,
80
- }, { http }, addMiddlewareForTags(channelConfig.middleware, channelConfig.tags), main);
81
+ }, { http }, combineMiddleware(PikkuWiringTypes.channel, channelConfig.name, {
82
+ wiringMiddleware: channelConfig.middleware,
83
+ wiringTags: channelConfig.tags,
84
+ httpMiddleware,
85
+ }), main);
81
86
  return channelHandler;
82
87
  };
@@ -1,10 +1,11 @@
1
+ import { PikkuWiringTypes } from '../../../types/core.types.js';
1
2
  import { closeSessionServices } from '../../../utils.js';
2
3
  import { processMessageHandlers } from '../channel-handler.js';
3
4
  import { openChannel } from '../channel-runner.js';
4
5
  import { createHTTPInteraction } from '../../http/http-runner.js';
5
6
  import { handleHTTPError } from '../../../handle-error.js';
6
7
  import { PikkuUserSessionService } from '../../../services/user-session-service.js';
7
- import { addMiddlewareForTags, runMiddleware, } from '../../../middleware-runner.js';
8
+ import { combineMiddleware, runMiddleware } from '../../../middleware-runner.js';
8
9
  import { pikkuState } from '../../../pikku-state.js';
9
10
  import { PikkuFetchHTTPRequest } from '../../http/pikku-fetch-http-request.js';
10
11
  import { runPikkuFuncDirectly } from '../../../function/function-runner.js';
@@ -34,7 +35,7 @@ export const runChannelConnect = async ({ singletonServices, channelId, channelO
34
35
  http = createHTTPInteraction(new PikkuFetchHTTPRequest(request), response);
35
36
  }
36
37
  const userSession = new PikkuUserSessionService(channelStore, channelId);
37
- const { channelConfig, openingData, meta } = await openChannel({
38
+ const { channelConfig, openingData, meta, httpMiddleware } = await openChannel({
38
39
  channelId,
39
40
  createSessionServices,
40
41
  request,
@@ -76,7 +77,11 @@ export const runChannelConnect = async ({ singletonServices, channelId, channelO
76
77
  await runMiddleware({
77
78
  ...singletonServices,
78
79
  userSession,
79
- }, { http }, addMiddlewareForTags(channelConfig.middleware, channelConfig.tags), main);
80
+ }, { http }, combineMiddleware(PikkuWiringTypes.channel, channelConfig.name, {
81
+ wiringMiddleware: channelConfig.middleware,
82
+ wiringTags: channelConfig.tags,
83
+ httpMiddleware,
84
+ }), main);
80
85
  };
81
86
  export const runChannelDisconnect = async ({ singletonServices, ...params }) => {
82
87
  let sessionServices;
@@ -1,14 +1,15 @@
1
- import { match } from 'path-to-regexp';
1
+ import { PikkuWiringTypes, } from '../../types/core.types.js';
2
2
  import { MissingSessionError, NotFoundError } from '../../errors/errors.js';
3
3
  import { closeSessionServices, createWeakUID, isSerializable, } from '../../utils.js';
4
4
  import { PikkuUserSessionService, } from '../../services/user-session-service.js';
5
- import { addMiddlewareForTags, runMiddleware } from '../../middleware-runner.js';
5
+ import { combineMiddleware, runMiddleware } from '../../middleware-runner.js';
6
6
  import { handleHTTPError } from '../../handle-error.js';
7
7
  import { pikkuState } from '../../pikku-state.js';
8
8
  import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js';
9
9
  import { PikkuFetchHTTPRequest } from './pikku-fetch-http-request.js';
10
10
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
11
11
  import { rpcService } from '../rpc/rpc-runner.js';
12
+ import { httpRouter } from './routers/http-router.js';
12
13
  /**
13
14
  * Registers middleware either globally or for a specific route.
14
15
  *
@@ -22,25 +23,20 @@ import { rpcService } from '../rpc/rpc-runner.js';
22
23
  */
23
24
  export const addHTTPMiddleware = (routeOrMiddleware, middleware) => {
24
25
  const middlewareStore = pikkuState('http', 'middleware');
25
- if (typeof routeOrMiddleware === 'string' && middleware) {
26
- const route = routeOrMiddleware;
27
- // Remove existing entry for this route if it exists
28
- const existingIndex = middlewareStore.findIndex((item) => item.route === route);
29
- if (existingIndex !== -1) {
30
- middlewareStore.splice(existingIndex, 1);
31
- }
32
- middlewareStore.push({ route, middleware });
26
+ let route = '*';
27
+ if (typeof routeOrMiddleware === 'string') {
28
+ route = routeOrMiddleware;
29
+ middleware = middleware;
33
30
  }
34
31
  else {
35
- // Handle global middleware
36
- const existingIndex = middlewareStore.findIndex((item) => item.route === '*');
37
- if (existingIndex !== -1) {
38
- middlewareStore.splice(existingIndex, 1);
39
- }
40
- middlewareStore.push({
41
- route: '*',
42
- middleware: routeOrMiddleware,
43
- });
32
+ middleware = routeOrMiddleware;
33
+ }
34
+ const currentMiddleware = middlewareStore.get(route);
35
+ if (currentMiddleware) {
36
+ middlewareStore.set(route, [...currentMiddleware, ...middleware]);
37
+ }
38
+ else {
39
+ middlewareStore.set(route, middleware);
44
40
  }
45
41
  };
46
42
  /**
@@ -60,7 +56,7 @@ export const addHTTPMiddleware = (routeOrMiddleware, middleware) => {
60
56
  */
61
57
  export const wireHTTP = (httpWiring) => {
62
58
  const httpMeta = pikkuState('http', 'meta');
63
- const routeMeta = httpMeta.find((meta) => meta.route === httpWiring.route && meta.method === httpWiring.method);
59
+ const routeMeta = httpMeta[httpWiring.method][httpWiring.route];
64
60
  if (!routeMeta) {
65
61
  throw new Error('Route metadata not found');
66
62
  }
@@ -85,37 +81,22 @@ export const wireHTTP = (httpWiring) => {
85
81
  * @returns {Object | undefined} An object with matched route details or undefined if no match.
86
82
  */
87
83
  const getMatchingRoute = (requestType, requestPath) => {
88
- const allRoutes = pikkuState('http', 'routes');
89
- const middleware = pikkuState('http', 'middleware');
90
- const routes = allRoutes.get(requestType.toLowerCase());
91
- if (!routes) {
92
- return undefined;
93
- }
94
- for (const route of routes.values()) {
95
- // Generate a matching function from the route pattern
96
- const matchFunc = match(`/${route.route}`.replace(/^\/\//, '/'), {
97
- decode: decodeURIComponent,
98
- });
99
- // Attempt to match the request path
100
- const matchedPath = matchFunc(requestPath.replace(/^\/\//, '/'));
101
- if (matchedPath) {
102
- // Aggregate global and route-specific middleware
103
- const globalMiddleware = middleware
104
- .filter((m) => m.route === '*' || new RegExp(m.route).test(route.route))
105
- .map((m) => m.middleware)
106
- .flat();
107
- const meta = pikkuState('http', 'meta').find((meta) => meta.route === route.route && meta.method === route.method);
108
- return {
109
- matchedPath,
110
- params: matchedPath.params,
111
- route,
112
- permissions: route.permissions,
113
- middleware: [...globalMiddleware, ...(route.middleware || [])],
114
- meta: meta,
115
- };
116
- }
84
+ const matchedPath = httpRouter.match(requestType.toLowerCase(), requestPath);
85
+ if (matchedPath) {
86
+ const route = pikkuState('http', 'routes')
87
+ .get(requestType.toLowerCase())
88
+ .get(matchedPath.route);
89
+ const meta = pikkuState('http', 'meta')[requestType.toLowerCase()][route.route];
90
+ return {
91
+ matchedPath,
92
+ params: matchedPath.params,
93
+ route,
94
+ permissions: route.permissions,
95
+ httpMiddleware: matchedPath.middleware,
96
+ middleware: route.middleware,
97
+ meta: meta,
98
+ };
117
99
  }
118
- return undefined;
119
100
  };
120
101
  /**
121
102
  * Combines the request and response objects into a single HTTP interaction object.
@@ -170,7 +151,7 @@ export const createHTTPInteraction = (request, response) => {
170
151
  * @throws Throws errors like MissingSessionError or ForbiddenError on validation failures.
171
152
  */
172
153
  const executeRouteWithMiddleware = async (services, matchedRoute, http, options) => {
173
- const { matchedPath, params, route, middleware, meta } = matchedRoute;
154
+ const { matchedPath, params, route, httpMiddleware, middleware, meta } = matchedRoute;
174
155
  const { singletonServices, userSession, createSessionServices, skipUserSession, requestId, } = services;
175
156
  const requiresSession = route.auth !== false;
176
157
  let sessionServices;
@@ -231,7 +212,7 @@ const executeRouteWithMiddleware = async (services, matchedRoute, http, options)
231
212
  channel,
232
213
  });
233
214
  };
234
- result = await runPikkuFunc(meta.pikkuFuncName, {
215
+ result = await runPikkuFunc(PikkuWiringTypes.http, `${meta.method}:${meta.route}`, meta.pikkuFuncName, {
235
216
  getAllServices,
236
217
  session,
237
218
  data,
@@ -250,8 +231,16 @@ const executeRouteWithMiddleware = async (services, matchedRoute, http, options)
250
231
  // TODO: Evaluate if the response stream should be explicitly ended.
251
232
  // http?.response?.end()
252
233
  };
234
+ // Get function config for middleware and tags
235
+ const funcConfig = pikkuState('function', 'functions').get(meta.pikkuFuncName);
253
236
  // Execute middleware, then run the main logic
254
- await runMiddleware({ ...singletonServices, userSession }, { http }, addMiddlewareForTags(middleware, route.tags), runMain);
237
+ await runMiddleware({ ...singletonServices, userSession }, { http }, combineMiddleware(PikkuWiringTypes.http, `${meta.method}:${meta.route}`, {
238
+ httpMiddleware,
239
+ wiringMiddleware: middleware,
240
+ wiringTags: route.tags,
241
+ funcMiddleware: funcConfig?.middleware,
242
+ funcTags: funcConfig?.tags,
243
+ }), runMain);
255
244
  return sessionServices ? { result, sessionServices } : { result };
256
245
  };
257
246
  /**
@@ -343,7 +332,8 @@ export const fetchData = async (request, response, { singletonServices, createSe
343
332
  }, ignoreMiddleware
344
333
  ? {
345
334
  ...matchedRoute,
346
- middleware: [],
335
+ middleware: undefined,
336
+ httpMiddleware: undefined,
347
337
  }
348
338
  : matchedRoute, http, { coerceDataFromSchema }));
349
339
  return result;
@@ -46,8 +46,8 @@ export type CoreHTTPFunction = {
46
46
  /**
47
47
  * Represents a http interaction within Pikku, including a request and response.
48
48
  */
49
- export interface PikkuHTTP {
50
- request?: PikkuHTTPRequest;
49
+ export interface PikkuHTTP<In = unknown> {
50
+ request?: PikkuHTTPRequest<In>;
51
51
  response?: PikkuHTTPResponse;
52
52
  }
53
53
  /**
@@ -147,7 +147,7 @@ export type HTTPWiringMeta = {
147
147
  tags?: string[];
148
148
  sse?: true;
149
149
  };
150
- export type HTTPWiringsMeta = Array<HTTPWiringMeta>;
150
+ export type HTTPWiringsMeta = Record<HTTPMethod, Record<string, HTTPWiringMeta>>;
151
151
  export type HTTPFunctionsMeta = Array<{
152
152
  name: string;
153
153
  inputs: string[] | null;
@@ -169,12 +169,12 @@ export interface PikkuHTTPRequest<In = unknown> {
169
169
  setParams(params: Record<string, string | string[] | undefined>): void;
170
170
  query(): PikkuQuery;
171
171
  }
172
- export interface PikkuHTTPResponse {
172
+ export interface PikkuHTTPResponse<Out = unknown> {
173
173
  status(code: number): this;
174
174
  cookie(name: string, value: string | null, options: SerializeOptions): this;
175
175
  header(name: string, value: string | string[]): this;
176
176
  arrayBuffer(data: ArrayBuffer | ArrayBufferView | Blob | string | FormData | URLSearchParams | ReadableStream): this;
177
- json(data: unknown): this;
177
+ json(data: Out): this;
178
178
  redirect(location: string, status?: number): this;
179
179
  close?: () => void;
180
180
  setMode?: (mode: 'stream') => void;
@@ -48,6 +48,9 @@ export class PikkuFetchHTTPRequest {
48
48
  * @returns An object containing the cookies.
49
49
  */
50
50
  cookie(cookieName) {
51
+ if (this.#cookies?.[cookieName]) {
52
+ return this.#cookies[cookieName];
53
+ }
51
54
  const cookieHeader = this.header('cookie');
52
55
  this.#cookies = cookieHeader ? parseCookie(cookieHeader) : {};
53
56
  return this.#cookies[cookieName] || null;
@@ -0,0 +1,12 @@
1
+ import { CorePikkuMiddleware } from '../../../types/core.types.js';
2
+ import { HTTPMethod } from '../http.types.js';
3
+ import { PathToRegexRouter } from './path-to-regex.js';
4
+ export type MatchResult = {
5
+ route: string;
6
+ params: Record<string, any>;
7
+ middleware?: CorePikkuMiddleware[];
8
+ } | null;
9
+ export interface Router {
10
+ match(method: HTTPMethod, path: string): MatchResult;
11
+ }
12
+ export declare const httpRouter: PathToRegexRouter;
@@ -0,0 +1,2 @@
1
+ import { PathToRegexRouter } from './path-to-regex.js';
2
+ export const httpRouter = new PathToRegexRouter();
@@ -0,0 +1,10 @@
1
+ import { MatchResult, Router } from './http-router.js';
2
+ import { HTTPMethod } from '../http.types.js';
3
+ export declare class PathToRegexRouter implements Router {
4
+ private compiledRoutes;
5
+ private staticRoutes;
6
+ private precompiledMiddleware;
7
+ private isInitialized;
8
+ initialize(): void;
9
+ match(method: HTTPMethod, path: string): MatchResult;
10
+ }
@@ -0,0 +1,118 @@
1
+ import { match } from 'path-to-regexp';
2
+ import { pikkuState } from '../../../pikku-state.js';
3
+ export class PathToRegexRouter {
4
+ compiledRoutes = new Map();
5
+ staticRoutes = new Map();
6
+ precompiledMiddleware = new Map();
7
+ isInitialized = false;
8
+ initialize() {
9
+ const routes = pikkuState('http', 'routes');
10
+ const channelRoutes = pikkuState('channel', 'channels');
11
+ const middlewareMap = pikkuState('http', 'middleware');
12
+ // Precompile middleware lookups
13
+ for (const [middlewareRoute, middlewareArray] of middlewareMap.entries()) {
14
+ this.precompiledMiddleware.set(middlewareRoute, middlewareArray);
15
+ }
16
+ // Helper function to compile routes for a given method
17
+ const compileRoutesForMethod = (method, routeEntries) => {
18
+ const methodCompiledRoutes = this.compiledRoutes.get(method) || new Map();
19
+ const methodStaticRoutes = this.staticRoutes.get(method) || new Map();
20
+ for (const [routePath] of routeEntries) {
21
+ // Normalize route path - ensure it starts with /
22
+ const normalizedRoutePath = routePath.startsWith('/')
23
+ ? routePath
24
+ : `/${routePath}`;
25
+ // Check if route is static (no parameters or wildcards)
26
+ const isStaticRoute = !/\*|:/.test(normalizedRoutePath);
27
+ // Precompute middleware for this route
28
+ const routeMiddleware = [];
29
+ // Add global middleware (*)
30
+ const globalMiddleware = this.precompiledMiddleware.get('*');
31
+ if (globalMiddleware) {
32
+ routeMiddleware.push(...globalMiddleware);
33
+ }
34
+ // Add route-specific middleware
35
+ for (const [middlewareRoute, middlewareArray,] of this.precompiledMiddleware.entries()) {
36
+ if (middlewareRoute !== '*') {
37
+ // Use regex test for pattern matching
38
+ try {
39
+ if (new RegExp(middlewareRoute).test(normalizedRoutePath)) {
40
+ routeMiddleware.push(...middlewareArray);
41
+ }
42
+ }
43
+ catch {
44
+ // If regex is invalid, do exact match
45
+ if (middlewareRoute === normalizedRoutePath) {
46
+ routeMiddleware.push(...middlewareArray);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ if (isStaticRoute) {
52
+ // Store static routes for O(1) lookup
53
+ methodStaticRoutes.set(normalizedRoutePath, {
54
+ route: routePath, // Keep the original route path for lookup in pikkuState
55
+ middleware: routeMiddleware,
56
+ });
57
+ }
58
+ else {
59
+ // Compile dynamic routes with path-to-regexp
60
+ const matcher = match(normalizedRoutePath, {
61
+ decode: decodeURIComponent,
62
+ });
63
+ methodCompiledRoutes.set(normalizedRoutePath, {
64
+ matcher,
65
+ route: routePath, // Keep the original route path for lookup in pikkuState
66
+ middleware: routeMiddleware,
67
+ });
68
+ }
69
+ }
70
+ this.compiledRoutes.set(method, methodCompiledRoutes);
71
+ this.staticRoutes.set(method, methodStaticRoutes);
72
+ };
73
+ // Precompile all HTTP route matchers
74
+ for (const [method, routeMap] of routes.entries()) {
75
+ compileRoutesForMethod(method, routeMap.entries());
76
+ }
77
+ // Precompile all channel route matchers (treating them as GET routes for WebSocket upgrades)
78
+ const channelRoutesArray = Array.from(channelRoutes.entries()).map(([, channel]) => [channel.route, channel]);
79
+ compileRoutesForMethod('get', channelRoutesArray);
80
+ this.isInitialized = true;
81
+ }
82
+ match(method, path) {
83
+ if (!this.isInitialized) {
84
+ this.initialize();
85
+ }
86
+ // Normalize path - ensure it starts with /
87
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
88
+ // First, try static routes for O(1) lookup
89
+ const methodStaticRoutes = this.staticRoutes.get(method);
90
+ if (methodStaticRoutes) {
91
+ const staticRoute = methodStaticRoutes.get(normalizedPath);
92
+ if (staticRoute) {
93
+ return {
94
+ route: staticRoute.route,
95
+ params: {},
96
+ middleware: staticRoute.middleware,
97
+ };
98
+ }
99
+ }
100
+ // If no static route matched, try dynamic routes
101
+ const methodRoutes = this.compiledRoutes.get(method);
102
+ if (!methodRoutes) {
103
+ return null;
104
+ }
105
+ // Try each compiled route for this method
106
+ for (const [, compiledRoute] of methodRoutes.entries()) {
107
+ const result = compiledRoute.matcher(normalizedPath);
108
+ if (result) {
109
+ return {
110
+ route: compiledRoute.route,
111
+ params: result.params,
112
+ middleware: compiledRoute.middleware,
113
+ };
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+ }
@@ -1,4 +1,4 @@
1
- import type { CoreServices, CoreSingletonServices, CoreUserSession, CreateSessionServices } from '../../types/core.types.js';
1
+ import { type CoreServices, type CoreSingletonServices, type CoreUserSession, type CreateSessionServices } from '../../types/core.types.js';
2
2
  import type { CoreMCPResource, CoreMCPTool, CoreMCPPrompt, JsonRpcRequest, JsonRpcResponse, JsonRpcErrorResponse, PikkuMCP } from './mcp.types.js';
3
3
  import type { CorePikkuFunctionSessionless } from '../../function/functions.types.js';
4
4
  export declare class MCPError extends Error {
@@ -1,10 +1,11 @@
1
+ import { PikkuWiringTypes, } from '../../types/core.types.js';
1
2
  import { getErrorResponse } from '../../errors/error-handler.js';
2
3
  import { closeSessionServices } from '../../utils.js';
3
4
  import { pikkuState } from '../../pikku-state.js';
4
5
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
5
6
  import { rpcService } from '../rpc/rpc-runner.js';
6
7
  import { BadRequestError, NotFoundError } from '../../errors/errors.js';
7
- import { addMiddlewareForTags, runMiddleware } from '../../middleware-runner.js';
8
+ import { combineMiddleware, runMiddleware } from '../../middleware-runner.js';
8
9
  export class MCPError extends Error {
9
10
  error;
10
11
  constructor(error) {
@@ -132,15 +133,20 @@ async function runMCPPikkuFunc(request, type, name, mcp, pikkuFuncName, { sessio
132
133
  mcp: interaction,
133
134
  });
134
135
  };
135
- result = await runPikkuFunc(pikkuFuncName, {
136
+ result = await runPikkuFunc(PikkuWiringTypes.mcp, `${type}:${name}`, pikkuFuncName, {
136
137
  getAllServices,
137
138
  session,
138
139
  data: request.params,
139
140
  tags: mcp.tags,
140
141
  });
141
142
  };
142
- // Get middleware for tags and run middleware
143
- await runMiddleware(singletonServices, { mcp: interaction }, addMiddlewareForTags(mcp.middleware, mcp.tags), runMain);
143
+ const funcConfig = pikkuState('function', 'functions').get(pikkuFuncName);
144
+ await runMiddleware(singletonServices, { mcp: interaction }, combineMiddleware(PikkuWiringTypes.mcp, `${type}:${name}`, {
145
+ wiringMiddleware: mcp.middleware,
146
+ wiringTags: mcp.tags,
147
+ funcMiddleware: funcConfig?.middleware,
148
+ funcTags: funcConfig?.tags,
149
+ }), runMain);
144
150
  return {
145
151
  id: request.id,
146
152
  result,
@@ -1,7 +1,8 @@
1
1
  import { getErrorResponse, PikkuError } from '../../errors/error-handler.js';
2
2
  import { pikkuState } from '../../pikku-state.js';
3
3
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
4
- import { addMiddlewareForTags, runMiddleware } from '../../middleware-runner.js';
4
+ import { PikkuWiringTypes, } from '../../types/core.types.js';
5
+ import { combineMiddleware, runMiddleware } from '../../middleware-runner.js';
5
6
  /**
6
7
  * Error class for queue processor not found
7
8
  */
@@ -106,15 +107,22 @@ export async function runQueueJob({ singletonServices, createSessionServices, jo
106
107
  : {}),
107
108
  });
108
109
  // Execute the pikku function with the job data
109
- result = await runPikkuFunc(processorMeta.pikkuFuncName, {
110
+ result = await runPikkuFunc(PikkuWiringTypes.queue, job.queueName, processorMeta.pikkuFuncName, {
110
111
  getAllServices,
111
112
  data: job.data,
112
113
  tags: queueWorker.tags,
113
114
  });
114
115
  logger.debug(`Successfully processed job ${job.id} in queue ${job.queueName}`);
115
116
  };
117
+ // Get function config for middleware and tags
118
+ const funcConfig = pikkuState('function', 'functions').get(processorMeta.pikkuFuncName);
116
119
  // Get middleware for tags and combine with queue-specific middleware
117
- await runMiddleware(singletonServices, { queue }, addMiddlewareForTags(queueWorker.middleware, queueWorker.tags), runMain);
120
+ await runMiddleware(singletonServices, { queue }, combineMiddleware(PikkuWiringTypes.queue, `${job.queueName}:${job.id}`, {
121
+ wiringMiddleware: queueWorker.middleware,
122
+ wiringTags: queueWorker.tags,
123
+ funcMiddleware: funcConfig?.middleware,
124
+ funcTags: funcConfig?.tags,
125
+ }), runMain);
118
126
  return result;
119
127
  }
120
128
  catch (error) {
@@ -13,13 +13,12 @@ export declare class PikkuRPCService {
13
13
  };
14
14
  logger: import("../../index.js").Logger;
15
15
  variables: import("../../services/variables-service.js").VariablesService;
16
- http?: import("../http/http.types.js").PikkuHTTP | undefined;
16
+ http?: import("../http/http.types.js").PikkuHTTP<unknown> | undefined;
17
17
  mcp?: import("../mcp/mcp.types.js").PikkuMCP;
18
18
  rpc?: import("./rpc-types.js").PikkuRPC;
19
19
  channel?: import("../channel/channel.types.js").PikkuChannel<unknown, unknown>;
20
20
  scheduledTask?: import("../scheduler/scheduler.types.js").PikkuScheduledTask | undefined;
21
21
  queue?: import("../queue/queue.types.js").PikkuQueue | undefined;
22
- s?: any;
23
22
  userSession?: import("../../index.js").UserSessionService<import("../../types/core.types.js").CoreUserSession> | undefined;
24
23
  };
25
24
  }
@@ -1,3 +1,4 @@
1
+ import { PikkuWiringTypes } from '../../types/core.types.js';
1
2
  import { runPikkuFunc } from '../../function/function-runner.js';
2
3
  import { pikkuState } from '../../pikku-state.js';
3
4
  import { ForbiddenError } from '../../errors/errors.js';
@@ -31,7 +32,7 @@ class ContextAwareRPCService {
31
32
  const session = await this.services.userSession?.get();
32
33
  const rpcDepth = this.services.rpc?.depth || 0;
33
34
  const pikkuFuncName = getPikkuFunctionName(funcName);
34
- return runPikkuFunc(pikkuFuncName, {
35
+ return runPikkuFunc(PikkuWiringTypes.rpc, pikkuFuncName, pikkuFuncName, {
35
36
  getAllServices: () => {
36
37
  this.services.rpc = this.services.rpc
37
38
  ? {
@@ -1,4 +1,4 @@
1
- import type { CoreServices, CoreSingletonServices, CoreUserSession, CreateSessionServices } from '../../types/core.types.js';
1
+ import { type CoreServices, type CoreSingletonServices, type CoreUserSession, type CreateSessionServices } from '../../types/core.types.js';
2
2
  import type { CoreScheduledTask } from './scheduler.types.js';
3
3
  import type { CorePikkuFunctionSessionless } from '../../function/functions.types.js';
4
4
  export type RunScheduledTasksParams = {