@pikku/core 0.6.20 → 0.6.22
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 +12 -0
- package/dist/channel/channel-handler.d.ts +2 -2
- package/dist/channel/channel-handler.js +49 -26
- package/dist/channel/channel-runner.d.ts +3 -3
- package/dist/channel/channel-runner.js +4 -4
- package/dist/channel/channel.types.d.ts +12 -19
- package/dist/channel/local/local-channel-runner.d.ts +1 -1
- package/dist/channel/local/local-channel-runner.js +14 -8
- package/dist/channel/serverless/serverless-channel-runner.js +12 -7
- package/dist/handle-error.js +8 -6
- package/dist/http/http-route-runner.d.ts +75 -18
- package/dist/http/http-route-runner.js +159 -64
- package/dist/http/http-routes.types.d.ts +24 -9
- package/dist/http/incomingmessage-to-request-convertor.d.ts +1 -0
- package/dist/http/incomingmessage-to-request-convertor.js +1 -0
- package/dist/http/index.d.ts +4 -3
- package/dist/http/index.js +4 -3
- package/dist/http/{pikku-http-abstract-request.d.ts → pikku-fetch-http-request.d.ts} +14 -20
- package/dist/http/pikku-fetch-http-request.js +92 -0
- package/dist/http/pikku-fetch-http-response.d.ts +14 -0
- package/dist/http/pikku-fetch-http-response.js +59 -0
- package/dist/middleware/auth-apikey.js +4 -4
- package/dist/middleware/auth-bearer.js +4 -4
- package/dist/middleware/auth-cookie.js +16 -25
- package/dist/middleware-runner.d.ts +1 -1
- package/dist/pikku-request.d.ts +3 -3
- package/dist/pikku-request.js +5 -5
- package/dist/scheduler/scheduler-runner.d.ts +1 -1
- package/dist/services/user-session-service.d.ts +9 -14
- package/dist/services/user-session-service.js +22 -22
- package/dist/types/core.types.d.ts +1 -1
- package/dist/types/functions.types.d.ts +7 -2
- package/lcov.info +1541 -1413
- package/package.json +3 -4
- package/src/channel/channel-handler.ts +82 -29
- package/src/channel/channel-runner.ts +6 -6
- package/src/channel/channel.types.ts +19 -23
- package/src/channel/local/local-channel-runner.test.ts +68 -23
- package/src/channel/local/local-channel-runner.ts +23 -9
- package/src/channel/serverless/serverless-channel-runner.ts +15 -6
- package/src/handle-error.ts +8 -6
- package/src/http/http-route-runner.test.ts +13 -57
- package/src/http/http-route-runner.ts +189 -92
- package/src/http/http-routes.types.ts +26 -9
- package/src/http/incomingmessage-to-request-convertor.ts +0 -0
- package/src/http/index.ts +4 -5
- package/src/http/{pikku-http-abstract-request.ts → pikku-fetch-http-request.ts} +48 -39
- package/src/http/pikku-fetch-http-response.ts +70 -0
- package/src/middleware/auth-apikey.ts +4 -4
- package/src/middleware/auth-bearer.ts +4 -4
- package/src/middleware/auth-cookie.ts +20 -28
- package/src/middleware-runner.ts +1 -1
- package/src/pikku-request.ts +8 -4
- package/src/scheduler/scheduler-runner.ts +0 -2
- package/src/services/user-session-service.ts +26 -28
- package/src/types/core.types.ts +1 -1
- package/src/types/functions.types.ts +19 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/http/pikku-http-abstract-request.js +0 -76
- package/dist/http/pikku-http-abstract-response.d.ts +0 -58
- package/dist/http/pikku-http-abstract-response.js +0 -54
- package/src/http/pikku-http-abstract-response.ts +0 -79
|
@@ -1,39 +1,20 @@
|
|
|
1
1
|
import { test, describe, beforeEach, afterEach } from 'node:test'
|
|
2
2
|
import * as assert from 'assert'
|
|
3
3
|
import { NotFoundError } from '../errors/errors.js'
|
|
4
|
-
import { PikkuHTTPAbstractRequest } from './pikku-http-abstract-request.js'
|
|
5
|
-
import { PikkuHTTPAbstractResponse } from './pikku-http-abstract-response.js'
|
|
6
4
|
import { JSONValue, PikkuMiddleware } from '../types/core.types.js'
|
|
7
|
-
import {
|
|
5
|
+
import { fetch, addRoute } from './http-route-runner.js'
|
|
8
6
|
import { resetPikkuState } from '../pikku-state.js'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
public getHeader(_headerName: string): string | undefined {
|
|
15
|
-
throw new Error('Method not implemented.')
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
class PikkuTestResponse extends PikkuHTTPAbstractResponse {
|
|
20
|
-
public setStatus(_status: number): void {
|
|
21
|
-
throw new Error('Method not implemented.')
|
|
22
|
-
}
|
|
23
|
-
public setJson(_body: JSONValue): void {
|
|
24
|
-
throw new Error('Method not implemented.')
|
|
25
|
-
}
|
|
26
|
-
public setResponse(_response: string | Buffer): void {
|
|
27
|
-
throw new Error('Method not implemented.')
|
|
28
|
-
}
|
|
29
|
-
}
|
|
7
|
+
import {
|
|
8
|
+
PikkuMockRequest,
|
|
9
|
+
PikkuMockResponse,
|
|
10
|
+
} from '../channel/local/local-channel-runner.test.js'
|
|
30
11
|
|
|
31
12
|
const sessionMiddleware: PikkuMiddleware = async (services, _, next) => {
|
|
32
13
|
services.userSessionService.set({ userId: 'test' } as any)
|
|
33
14
|
await next()
|
|
34
15
|
}
|
|
35
16
|
|
|
36
|
-
describe('
|
|
17
|
+
describe('fetch', () => {
|
|
37
18
|
let singletonServices: any
|
|
38
19
|
let createSessionServices: any
|
|
39
20
|
let request: any
|
|
@@ -51,8 +32,8 @@ describe('runHTTPRoute', () => {
|
|
|
51
32
|
}
|
|
52
33
|
|
|
53
34
|
createSessionServices = async () => ({})
|
|
54
|
-
request = new
|
|
55
|
-
response = new
|
|
35
|
+
request = new PikkuMockRequest('/test', 'get')
|
|
36
|
+
response = new PikkuMockResponse()
|
|
56
37
|
|
|
57
38
|
request.getData = async () => ({})
|
|
58
39
|
request.getHeader = () => 'application/json'
|
|
@@ -63,18 +44,11 @@ describe('runHTTPRoute', () => {
|
|
|
63
44
|
afterEach(() => {})
|
|
64
45
|
|
|
65
46
|
test('should throw RouteNotFoundError when no matching route is found', async () => {
|
|
66
|
-
const apiRoute = '/test'
|
|
67
|
-
const apiType = 'get'
|
|
68
|
-
|
|
69
47
|
await assert.rejects(
|
|
70
48
|
async () =>
|
|
71
|
-
|
|
72
|
-
request,
|
|
73
|
-
response,
|
|
49
|
+
fetch(request, {
|
|
74
50
|
singletonServices,
|
|
75
51
|
createSessionServices,
|
|
76
|
-
route: apiRoute,
|
|
77
|
-
method: apiType,
|
|
78
52
|
bubbleErrors: true,
|
|
79
53
|
}),
|
|
80
54
|
NotFoundError
|
|
@@ -82,8 +56,6 @@ describe('runHTTPRoute', () => {
|
|
|
82
56
|
})
|
|
83
57
|
|
|
84
58
|
test('should call the route function and return its result when a matching route is found', async () => {
|
|
85
|
-
const apiRoute = '/test'
|
|
86
|
-
const apiType = 'get'
|
|
87
59
|
const routeFunc = async () => ({ success: true })
|
|
88
60
|
addRoute({
|
|
89
61
|
route: 'test',
|
|
@@ -92,21 +64,15 @@ describe('runHTTPRoute', () => {
|
|
|
92
64
|
middleware: [sessionMiddleware],
|
|
93
65
|
})
|
|
94
66
|
|
|
95
|
-
const result = await
|
|
96
|
-
request,
|
|
97
|
-
response,
|
|
67
|
+
const result = await fetch(request, {
|
|
98
68
|
singletonServices,
|
|
99
69
|
createSessionServices,
|
|
100
|
-
route: apiRoute,
|
|
101
|
-
method: apiType,
|
|
102
70
|
})
|
|
103
71
|
|
|
104
|
-
assert.deepStrictEqual(result, { success: true })
|
|
72
|
+
assert.deepStrictEqual(await result.json(), { success: true })
|
|
105
73
|
})
|
|
106
74
|
|
|
107
75
|
test('should verify permissions if provided', async () => {
|
|
108
|
-
const apiRoute = '/test'
|
|
109
|
-
const apiType = 'get'
|
|
110
76
|
const permissions = { test: async () => true }
|
|
111
77
|
const routeFunc = async () => ({ success: true })
|
|
112
78
|
|
|
@@ -118,21 +84,15 @@ describe('runHTTPRoute', () => {
|
|
|
118
84
|
middleware: [sessionMiddleware],
|
|
119
85
|
})
|
|
120
86
|
|
|
121
|
-
await
|
|
122
|
-
request,
|
|
123
|
-
response,
|
|
87
|
+
await fetch(request, {
|
|
124
88
|
singletonServices,
|
|
125
89
|
createSessionServices,
|
|
126
|
-
route: apiRoute,
|
|
127
|
-
method: apiType,
|
|
128
90
|
})
|
|
129
91
|
|
|
130
92
|
assert.strictEqual(await permissions.test(), true)
|
|
131
93
|
})
|
|
132
94
|
|
|
133
95
|
test('should handle errors and set appropriate response', async () => {
|
|
134
|
-
const apiRoute = '/test'
|
|
135
|
-
const apiType = 'get'
|
|
136
96
|
const error = new Error('Test error')
|
|
137
97
|
const routeFunc = async () => {
|
|
138
98
|
throw error
|
|
@@ -146,13 +106,9 @@ describe('runHTTPRoute', () => {
|
|
|
146
106
|
|
|
147
107
|
await assert.rejects(
|
|
148
108
|
async () =>
|
|
149
|
-
|
|
150
|
-
request,
|
|
151
|
-
response,
|
|
109
|
+
fetch(request, {
|
|
152
110
|
singletonServices,
|
|
153
111
|
createSessionServices,
|
|
154
|
-
route: apiRoute,
|
|
155
|
-
method: apiType,
|
|
156
112
|
bubbleErrors: true,
|
|
157
113
|
}),
|
|
158
114
|
error
|
|
@@ -4,30 +4,35 @@ import {
|
|
|
4
4
|
RunRouteOptions,
|
|
5
5
|
RunRouteParams,
|
|
6
6
|
PikkuHTTP,
|
|
7
|
+
PikkuHTTPRequest,
|
|
8
|
+
PikkuHTTPResponse,
|
|
7
9
|
} from './http-routes.types.js'
|
|
8
10
|
import { PikkuMiddleware, SessionServices } from '../types/core.types.js'
|
|
9
11
|
import { match } from 'path-to-regexp'
|
|
10
|
-
import { PikkuHTTPAbstractRequest } from './pikku-http-abstract-request.js'
|
|
11
|
-
import { PikkuHTTPAbstractResponse } from './pikku-http-abstract-response.js'
|
|
12
12
|
import {
|
|
13
13
|
ForbiddenError,
|
|
14
14
|
MissingSessionError,
|
|
15
15
|
NotFoundError,
|
|
16
16
|
} from '../errors/errors.js'
|
|
17
17
|
import { closeSessionServices } from '../utils.js'
|
|
18
|
-
import { PikkuRequest } from '../pikku-request.js'
|
|
19
|
-
import { PikkuResponse } from '../pikku-response.js'
|
|
20
18
|
import { coerceQueryStringToArray, validateSchema } from '../schema.js'
|
|
21
|
-
import {
|
|
19
|
+
import { PikkuUserSessionService } from '../services/user-session-service.js'
|
|
22
20
|
import { runMiddleware } from '../middleware-runner.js'
|
|
23
21
|
import { handleError } from '../handle-error.js'
|
|
24
22
|
import { pikkuState } from '../pikku-state.js'
|
|
23
|
+
import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js'
|
|
24
|
+
import { PikkuFetchHTTPRequest } from './pikku-fetch-http-request.js'
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
27
|
+
* Registers middleware either globally or for a specific route.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* When a string route pattern is provided along with middleware, the middleware
|
|
30
|
+
* is applied only to that route. Otherwise, if an array is provided, it is treated
|
|
31
|
+
* as global middleware (applied to all routes).
|
|
32
|
+
*
|
|
33
|
+
* @template APIMiddleware The middleware type.
|
|
34
|
+
* @param {APIMiddleware[] | string} routeOrMiddleware - Either a global middleware array or a route pattern string.
|
|
35
|
+
* @param {APIMiddleware[]} [middleware] - The middleware array to apply when a route pattern is specified.
|
|
31
36
|
*/
|
|
32
37
|
export const addMiddleware = <APIMiddleware extends PikkuMiddleware>(
|
|
33
38
|
routeOrMiddleware: APIMiddleware[] | string,
|
|
@@ -47,9 +52,19 @@ export const addMiddleware = <APIMiddleware extends PikkuMiddleware>(
|
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/**
|
|
50
|
-
*
|
|
55
|
+
* Adds a new route to the global HTTP route registry.
|
|
56
|
+
*
|
|
57
|
+
* The route configuration includes the HTTP method, route path, permissions,
|
|
58
|
+
* middleware, and the handler function that implements the route's logic.
|
|
51
59
|
*
|
|
52
|
-
* @
|
|
60
|
+
* @template In Expected input type.
|
|
61
|
+
* @template Out Expected output type.
|
|
62
|
+
* @template Route Route pattern as a string.
|
|
63
|
+
* @template APIFunction Type for the route handler function.
|
|
64
|
+
* @template APIFunctionSessionless Type for a sessionless handler.
|
|
65
|
+
* @template APIPermission Type representing required permissions.
|
|
66
|
+
* @template APIMiddleware Middleware type to be used with the route.
|
|
67
|
+
* @param {CoreHTTPFunctionRoute<In, Out, Route, APIFunction, APIFunctionSessionless, APIPermission, APIMiddleware>} route - The route configuration object.
|
|
53
68
|
*/
|
|
54
69
|
export const addRoute = <
|
|
55
70
|
In,
|
|
@@ -74,11 +89,15 @@ export const addRoute = <
|
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
/**
|
|
77
|
-
*
|
|
92
|
+
* Finds a matching route based on the HTTP method and URL path.
|
|
78
93
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
94
|
+
* Iterates over all registered routes, skipping those with a mismatched method.
|
|
95
|
+
* When a route pattern matches the incoming request path, this function aggregates
|
|
96
|
+
* any global middleware along with route-specific middleware and identifies any input schema.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} requestType - The HTTP method (e.g., GET, POST).
|
|
99
|
+
* @param {string} requestPath - The URL path of the incoming request.
|
|
100
|
+
* @returns {Object | undefined} An object with matched route details or undefined if no match.
|
|
82
101
|
*/
|
|
83
102
|
const getMatchingRoute = (requestType: string, requestPath: string) => {
|
|
84
103
|
const routes = pikkuState('http', 'routes')
|
|
@@ -86,27 +105,27 @@ const getMatchingRoute = (requestType: string, requestPath: string) => {
|
|
|
86
105
|
const routesMeta = pikkuState('http', 'meta')
|
|
87
106
|
|
|
88
107
|
for (const route of routes) {
|
|
89
|
-
// Skip routes that don't match the
|
|
108
|
+
// Skip routes that don't match the HTTP method
|
|
90
109
|
if (route.method !== requestType.toLowerCase()) {
|
|
91
110
|
continue
|
|
92
111
|
}
|
|
93
112
|
|
|
94
|
-
//
|
|
113
|
+
// Generate a matching function from the route pattern
|
|
95
114
|
const matchFunc = match(`/${route.route}`.replace(/^\/\//, '/'), {
|
|
96
115
|
decode: decodeURIComponent,
|
|
97
116
|
})
|
|
98
117
|
|
|
99
|
-
//
|
|
118
|
+
// Attempt to match the request path
|
|
100
119
|
const matchedPath = matchFunc(requestPath.replace(/^\/\//, '/'))
|
|
101
120
|
|
|
102
121
|
if (matchedPath) {
|
|
103
|
-
//
|
|
122
|
+
// Aggregate global and route-specific middleware
|
|
104
123
|
const globalMiddleware = middleware
|
|
105
124
|
.filter((m) => m.route === '*' || new RegExp(m.route).test(route.route))
|
|
106
125
|
.map((m) => m.middleware)
|
|
107
126
|
.flat()
|
|
108
127
|
|
|
109
|
-
//
|
|
128
|
+
// Extract associated schema information if available
|
|
110
129
|
const schemaName = routesMeta.find(
|
|
111
130
|
(routeMeta) =>
|
|
112
131
|
routeMeta.method === route.method && routeMeta.route === route.route
|
|
@@ -125,27 +144,27 @@ const getMatchingRoute = (requestType: string, requestPath: string) => {
|
|
|
125
144
|
}
|
|
126
145
|
|
|
127
146
|
/**
|
|
128
|
-
*
|
|
147
|
+
* Combines the request and response objects into a single HTTP interaction object.
|
|
148
|
+
*
|
|
149
|
+
* This utility function creates an object that holds both the HTTP request and response,
|
|
150
|
+
* which simplifies passing these around through middleware and route execution.
|
|
129
151
|
*
|
|
130
|
-
* @param {
|
|
131
|
-
* @param {
|
|
132
|
-
* @returns {PikkuHTTP | undefined} HTTP interaction object or undefined
|
|
152
|
+
* @param {PikkuHTTPRequest | undefined} request - The HTTP request object.
|
|
153
|
+
* @param {PikkuHTTPResponse | undefined} response - The HTTP response object.
|
|
154
|
+
* @returns {PikkuHTTP | undefined} The combined HTTP interaction object or undefined if none provided.
|
|
133
155
|
*/
|
|
134
156
|
export const createHTTPInteraction = (
|
|
135
|
-
request:
|
|
136
|
-
response:
|
|
157
|
+
request: PikkuHTTPRequest | undefined,
|
|
158
|
+
response: PikkuHTTPResponse | undefined
|
|
137
159
|
): PikkuHTTP | undefined => {
|
|
138
160
|
let http: PikkuHTTP | undefined = undefined
|
|
139
161
|
|
|
140
|
-
if (
|
|
141
|
-
request instanceof PikkuHTTPAbstractRequest ||
|
|
142
|
-
response instanceof PikkuHTTPAbstractResponse
|
|
143
|
-
) {
|
|
162
|
+
if (request || response) {
|
|
144
163
|
http = {}
|
|
145
|
-
if (request
|
|
164
|
+
if (request) {
|
|
146
165
|
http.request = request
|
|
147
166
|
}
|
|
148
|
-
if (response
|
|
167
|
+
if (response) {
|
|
149
168
|
http.response = response
|
|
150
169
|
}
|
|
151
170
|
}
|
|
@@ -154,19 +173,29 @@ export const createHTTPInteraction = (
|
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
/**
|
|
157
|
-
*
|
|
176
|
+
* Validates the input data and executes the route handler with associated middleware.
|
|
158
177
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
178
|
+
* This function is the central execution point for a route. It performs these steps:
|
|
179
|
+
* 1. Sets URL parameters on the request.
|
|
180
|
+
* 2. Validates the user session if required.
|
|
181
|
+
* 3. Creates session-specific services.
|
|
182
|
+
* 4. Validates the incoming data against a schema.
|
|
183
|
+
* 5. Optionally coerces query string values to arrays.
|
|
184
|
+
* 6. Checks route-specific permissions.
|
|
185
|
+
* 7. Executes the route handler.
|
|
186
|
+
* 8. Sends the appropriate response.
|
|
187
|
+
*
|
|
188
|
+
* @param {Object} services - A collection of shared services and utilities.
|
|
189
|
+
* @param {Object} matchedRoute - Contains route details, URL parameters, middleware, and optional schema.
|
|
190
|
+
* @param {PikkuHTTP | undefined} http - The HTTP interaction object.
|
|
191
|
+
* @param {Object} options - Options for route execution (e.g., whether to coerce query strings to arrays).
|
|
192
|
+
* @returns {Promise<any>} An object containing the route handler result and session services (if any).
|
|
193
|
+
* @throws Throws errors like MissingSessionError or ForbiddenError on validation failures.
|
|
164
194
|
*/
|
|
165
195
|
const executeRouteWithMiddleware = async (
|
|
166
196
|
services: {
|
|
167
197
|
singletonServices: any
|
|
168
198
|
userSessionService: any
|
|
169
|
-
context: Map<string, unknown>
|
|
170
199
|
createSessionServices: Function
|
|
171
200
|
skipUserSession: boolean
|
|
172
201
|
},
|
|
@@ -186,7 +215,6 @@ const executeRouteWithMiddleware = async (
|
|
|
186
215
|
const {
|
|
187
216
|
singletonServices,
|
|
188
217
|
userSessionService,
|
|
189
|
-
context,
|
|
190
218
|
createSessionServices,
|
|
191
219
|
skipUserSession,
|
|
192
220
|
} = services
|
|
@@ -195,17 +223,18 @@ const executeRouteWithMiddleware = async (
|
|
|
195
223
|
let sessionServices: any
|
|
196
224
|
let result: any
|
|
197
225
|
|
|
226
|
+
// Attach URL parameters to the request object
|
|
198
227
|
http?.request?.setParams(params)
|
|
199
228
|
|
|
200
229
|
singletonServices.logger.info(
|
|
201
230
|
`Matched route: ${route.route} | method: ${route.method.toUpperCase()} | auth: ${requiresSession.toString()}`
|
|
202
231
|
)
|
|
203
232
|
|
|
204
|
-
// Main route execution
|
|
233
|
+
// Main route execution logic wrapped for middleware handling
|
|
205
234
|
const runMain = async () => {
|
|
206
235
|
const session = userSessionService.get()
|
|
207
236
|
|
|
208
|
-
//
|
|
237
|
+
// Ensure session is available when required
|
|
209
238
|
if (skipUserSession && requiresSession) {
|
|
210
239
|
throw new Error(
|
|
211
240
|
"Can't skip trying to get user session if auth is required"
|
|
@@ -220,29 +249,39 @@ const executeRouteWithMiddleware = async (
|
|
|
220
249
|
throw new MissingSessionError()
|
|
221
250
|
}
|
|
222
251
|
|
|
223
|
-
// Create session services
|
|
252
|
+
// Create session-specific services for handling the request
|
|
224
253
|
sessionServices = await createSessionServices(
|
|
225
|
-
{ ...singletonServices,
|
|
254
|
+
{ ...singletonServices, userSessionService },
|
|
226
255
|
{ http },
|
|
227
256
|
session
|
|
228
257
|
)
|
|
229
258
|
|
|
230
|
-
const allServices = {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
259
|
+
const allServices = {
|
|
260
|
+
...singletonServices,
|
|
261
|
+
...sessionServices,
|
|
262
|
+
http,
|
|
263
|
+
}
|
|
264
|
+
const data = await http?.request?.data()
|
|
265
|
+
|
|
266
|
+
// Validate request data against the defined schema, if any
|
|
267
|
+
try {
|
|
268
|
+
console.log(schemaName, data)
|
|
269
|
+
validateSchema(
|
|
270
|
+
singletonServices.logger,
|
|
271
|
+
singletonServices.schemaService,
|
|
272
|
+
schemaName,
|
|
273
|
+
data
|
|
274
|
+
)
|
|
275
|
+
} catch (e) {
|
|
276
|
+
// TODO: Implement proper handling for schema validation failures
|
|
277
|
+
}
|
|
240
278
|
|
|
279
|
+
// Coerce query string parameters to arrays if specified by the schema
|
|
241
280
|
if (options.coerceToArray && schemaName) {
|
|
242
281
|
coerceQueryStringToArray(schemaName, data)
|
|
243
282
|
}
|
|
244
283
|
|
|
245
|
-
//
|
|
284
|
+
// Execute permission checks
|
|
246
285
|
const permissioned = await verifyPermissions(
|
|
247
286
|
route.permissions,
|
|
248
287
|
allServices,
|
|
@@ -254,24 +293,26 @@ const executeRouteWithMiddleware = async (
|
|
|
254
293
|
throw new ForbiddenError('Permission denied')
|
|
255
294
|
}
|
|
256
295
|
|
|
257
|
-
//
|
|
296
|
+
// Invoke the actual route handler function
|
|
258
297
|
result = await route.func(allServices, data, session!)
|
|
259
298
|
|
|
260
|
-
//
|
|
299
|
+
// Respond with either a binary or JSON response based on configuration
|
|
261
300
|
if (route.returnsJSON === false) {
|
|
262
|
-
http?.response?.
|
|
301
|
+
http?.response?.arrayBuffer(result)
|
|
263
302
|
} else {
|
|
264
|
-
http?.response?.
|
|
303
|
+
http?.response?.json(result)
|
|
265
304
|
}
|
|
266
305
|
|
|
267
|
-
http?.response?.
|
|
268
|
-
|
|
306
|
+
http?.response?.status(200)
|
|
307
|
+
// TODO: Evaluate if the response stream should be explicitly ended.
|
|
308
|
+
// http?.response?.end()
|
|
269
309
|
|
|
270
310
|
return result
|
|
271
311
|
}
|
|
272
312
|
|
|
313
|
+
// Execute middleware, then run the main logic
|
|
273
314
|
await runMiddleware(
|
|
274
|
-
{ ...singletonServices,
|
|
315
|
+
{ ...singletonServices, userSessionService },
|
|
275
316
|
{ http },
|
|
276
317
|
middleware,
|
|
277
318
|
runMain
|
|
@@ -281,41 +322,98 @@ const executeRouteWithMiddleware = async (
|
|
|
281
322
|
}
|
|
282
323
|
|
|
283
324
|
/**
|
|
284
|
-
*
|
|
325
|
+
* Executes an HTTP route for a given Fetch API request.
|
|
326
|
+
*
|
|
327
|
+
* This function wraps the entire lifecycle of handling an HTTP request:
|
|
328
|
+
* - Matching the request to a registered route.
|
|
329
|
+
* - Validating input and session state.
|
|
330
|
+
* - Running middleware and the route handler.
|
|
331
|
+
* - Handling errors and forming the response.
|
|
285
332
|
*
|
|
286
|
-
* @
|
|
287
|
-
* @
|
|
288
|
-
* @
|
|
333
|
+
* @template In Expected input data type.
|
|
334
|
+
* @template Out Expected output data type.
|
|
335
|
+
* @param {Request} request - The native Fetch API Request object.
|
|
336
|
+
* @param {RunRouteOptions & RunRouteParams} params - Additional options including services and session management.
|
|
337
|
+
* @returns {Promise<Response>} A promise that resolves to a Fetch API Response object.
|
|
289
338
|
*/
|
|
290
|
-
export const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
339
|
+
export const fetch = async <In, Out>(
|
|
340
|
+
request: Request,
|
|
341
|
+
params: RunRouteOptions & RunRouteParams
|
|
342
|
+
): Promise<Response> => {
|
|
343
|
+
const pikkuResponse = new PikkuFetchHTTPResponse()
|
|
344
|
+
await fetchData<In, Out>(request, pikkuResponse, params)
|
|
345
|
+
return pikkuResponse.toResponse()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Executes an HTTP route using a Pikku-specific request wrapper.
|
|
350
|
+
*
|
|
351
|
+
* This variant accepts either a native Request or a PikkuHTTPRequest object and returns
|
|
352
|
+
* a PikkuFetchHTTPResponse for further manipulation if needed.
|
|
353
|
+
*
|
|
354
|
+
* @template In Expected input data type.
|
|
355
|
+
* @template Out Expected output data type.
|
|
356
|
+
* @param {Request | PikkuHTTPRequest} request - The request object.
|
|
357
|
+
* @param {RunRouteOptions & RunRouteParams} params - Execution options including services and session configuration.
|
|
358
|
+
* @returns {Promise<PikkuFetchHTTPResponse>} A promise that resolves to a PikkuFetchHTTPResponse object.
|
|
359
|
+
*/
|
|
360
|
+
export const pikkuFetch = async <In, Out>(
|
|
361
|
+
request: Request | PikkuHTTPRequest,
|
|
362
|
+
params: RunRouteOptions & RunRouteParams
|
|
363
|
+
): Promise<PikkuFetchHTTPResponse> => {
|
|
364
|
+
const pikkuResponse = new PikkuFetchHTTPResponse()
|
|
365
|
+
await fetchData<In, Out>(request, pikkuResponse, params)
|
|
366
|
+
return pikkuResponse
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Core function to process an HTTP request through route matching, validation,
|
|
371
|
+
* middleware execution, error handling, and session service cleanup.
|
|
372
|
+
*
|
|
373
|
+
* This function does the following:
|
|
374
|
+
* - Wraps the incoming request and response into an HTTP interaction object.
|
|
375
|
+
* - Determines the correct route based on HTTP method and path.
|
|
376
|
+
* - Executes middleware and the route handler.
|
|
377
|
+
* - Catches and handles errors, optionally bubbling them if configured.
|
|
378
|
+
* - Cleans up any session services created during processing.
|
|
379
|
+
*
|
|
380
|
+
* @template In Expected input data type.
|
|
381
|
+
* @template Out Expected output data type.
|
|
382
|
+
* @param {Request | PikkuHTTPRequest} request - The incoming HTTP request.
|
|
383
|
+
* @param {PikkuHTTPResponse} response - The response object to be populated.
|
|
384
|
+
* @param {RunRouteOptions & RunRouteParams} options - Options such as singleton services, session handling, and error configuration.
|
|
385
|
+
* @returns {Promise<Out | void>} The output from the route handler or void if an error occurred.
|
|
386
|
+
*/
|
|
387
|
+
export const fetchData = async <In, Out>(
|
|
388
|
+
request: Request | PikkuHTTPRequest,
|
|
389
|
+
response: PikkuHTTPResponse,
|
|
390
|
+
{
|
|
391
|
+
singletonServices,
|
|
392
|
+
createSessionServices,
|
|
393
|
+
skipUserSession = false,
|
|
394
|
+
respondWith404 = true,
|
|
395
|
+
logWarningsForStatusCodes = [],
|
|
396
|
+
coerceToArray = false,
|
|
397
|
+
bubbleErrors = false,
|
|
398
|
+
}: RunRouteOptions & RunRouteParams
|
|
399
|
+
): Promise<Out | void> => {
|
|
400
|
+
const userSessionService = new PikkuUserSessionService()
|
|
308
401
|
let sessionServices: SessionServices<typeof singletonServices> | undefined
|
|
309
402
|
let result: Out
|
|
310
403
|
|
|
311
|
-
//
|
|
312
|
-
const http = createHTTPInteraction(
|
|
404
|
+
// Combine the request and response into one interaction object
|
|
405
|
+
const http = createHTTPInteraction(
|
|
406
|
+
request instanceof Request ? new PikkuFetchHTTPRequest(request) : request,
|
|
407
|
+
response
|
|
408
|
+
)
|
|
409
|
+
const apiType = http!.request!.method()
|
|
410
|
+
const apiRoute = http!.request!.path()
|
|
313
411
|
|
|
314
|
-
//
|
|
412
|
+
// Locate the matching route based on the HTTP method and path
|
|
315
413
|
const matchedRoute = getMatchingRoute(apiType, apiRoute)
|
|
316
414
|
|
|
317
415
|
try {
|
|
318
|
-
//
|
|
416
|
+
// If no route matches, log the occurrence and throw a NotFoundError
|
|
319
417
|
if (!matchedRoute) {
|
|
320
418
|
singletonServices.logger.info({
|
|
321
419
|
message: 'Route not found',
|
|
@@ -325,12 +423,11 @@ export const runHTTPRoute = async <In, Out>({
|
|
|
325
423
|
throw new NotFoundError(`Route not found: ${apiRoute}`)
|
|
326
424
|
}
|
|
327
425
|
|
|
328
|
-
// Execute route with middleware
|
|
426
|
+
// Execute the matched route along with its middleware and session management
|
|
329
427
|
;({ result, sessionServices } = await executeRouteWithMiddleware(
|
|
330
428
|
{
|
|
331
429
|
singletonServices,
|
|
332
430
|
userSessionService,
|
|
333
|
-
context,
|
|
334
431
|
createSessionServices,
|
|
335
432
|
skipUserSession,
|
|
336
433
|
},
|
|
@@ -341,18 +438,18 @@ export const runHTTPRoute = async <In, Out>({
|
|
|
341
438
|
|
|
342
439
|
return result
|
|
343
440
|
} catch (e: any) {
|
|
344
|
-
// Handle and
|
|
441
|
+
// Handle errors and, depending on configuration, bubble them up or respond with an error
|
|
345
442
|
handleError(
|
|
346
443
|
e,
|
|
347
444
|
http,
|
|
348
|
-
context.get('trackingId'),
|
|
445
|
+
'111', // TODO: context.get('trackingId'),
|
|
349
446
|
singletonServices.logger,
|
|
350
447
|
logWarningsForStatusCodes,
|
|
351
448
|
respondWith404,
|
|
352
449
|
bubbleErrors
|
|
353
450
|
)
|
|
354
451
|
} finally {
|
|
355
|
-
// Clean up session services
|
|
452
|
+
// Clean up any session-specific services created during processing
|
|
356
453
|
if (sessionServices) {
|
|
357
454
|
await closeSessionServices(singletonServices.logger, sessionServices)
|
|
358
455
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SerializeOptions } from 'cookie'
|
|
1
2
|
import { PikkuError } from '../errors/error-handler.js'
|
|
2
3
|
import {
|
|
3
4
|
APIDocs,
|
|
@@ -12,10 +13,6 @@ import {
|
|
|
12
13
|
CoreAPIFunctionSessionless,
|
|
13
14
|
CoreAPIPermission,
|
|
14
15
|
} from '../types/functions.types.js'
|
|
15
|
-
import { PikkuRequest } from '../pikku-request.js'
|
|
16
|
-
import { PikkuResponse } from '../pikku-response.js'
|
|
17
|
-
import { PikkuHTTPAbstractRequest } from './pikku-http-abstract-request.js'
|
|
18
|
-
import { PikkuHTTPAbstractResponse } from './pikku-http-abstract-response.js'
|
|
19
16
|
|
|
20
17
|
type ExtractRouteParams<S extends string> =
|
|
21
18
|
S extends `${string}:${infer Param}/${infer Rest}`
|
|
@@ -37,10 +34,8 @@ export type RunRouteOptions = Partial<{
|
|
|
37
34
|
bubbleErrors: boolean
|
|
38
35
|
}>
|
|
39
36
|
|
|
40
|
-
export type RunRouteParams
|
|
37
|
+
export type RunRouteParams = {
|
|
41
38
|
singletonServices: CoreSingletonServices
|
|
42
|
-
request: PikkuRequest<In> | PikkuHTTPAbstractRequest<In>
|
|
43
|
-
response?: PikkuResponse | PikkuHTTPAbstractResponse | undefined
|
|
44
39
|
createSessionServices: CreateSessionServices<
|
|
45
40
|
CoreSingletonServices,
|
|
46
41
|
CoreServices<CoreSingletonServices>,
|
|
@@ -80,8 +75,8 @@ export type CoreHTTPFunction = {
|
|
|
80
75
|
* Represents a http interaction within Pikku, including a request and response.
|
|
81
76
|
*/
|
|
82
77
|
export interface PikkuHTTP {
|
|
83
|
-
request?:
|
|
84
|
-
response?:
|
|
78
|
+
request?: PikkuHTTPRequest
|
|
79
|
+
response?: PikkuHTTPResponse
|
|
85
80
|
}
|
|
86
81
|
|
|
87
82
|
/**
|
|
@@ -192,3 +187,25 @@ export type HTTPRouteMiddleware = {
|
|
|
192
187
|
route: string
|
|
193
188
|
middleware: PikkuMiddleware[]
|
|
194
189
|
}
|
|
190
|
+
|
|
191
|
+
export interface PikkuHTTPRequest<In = unknown> {
|
|
192
|
+
method(): HTTPMethod
|
|
193
|
+
path(): string
|
|
194
|
+
data(): Promise<In>
|
|
195
|
+
json(): Promise<unknown>
|
|
196
|
+
arrayBuffer(): Promise<ArrayBuffer>
|
|
197
|
+
header(headerName: string): string | null
|
|
198
|
+
cookie(name?: string): string | null
|
|
199
|
+
params(): Partial<Record<string, string | string[]>>
|
|
200
|
+
setParams(params: Record<string, string | string[] | undefined>): void
|
|
201
|
+
query(): PikkuQuery
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface PikkuHTTPResponse {
|
|
205
|
+
status(code: number): this
|
|
206
|
+
cookie(name: string, value: string | null, options: SerializeOptions): this
|
|
207
|
+
header(name: string, value: string | string[]): this
|
|
208
|
+
arrayBuffer(data: XMLHttpRequestBodyInit): this
|
|
209
|
+
json(data: unknown): this
|
|
210
|
+
redirect(location: string, status?: number): this
|
|
211
|
+
}
|