@pikku/core 0.12.20 → 0.12.21
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 +11 -0
- package/dist/dev/hot-reload.js +20 -6
- package/dist/errors/error-handler.d.ts +1 -1
- package/dist/errors/error-handler.js +2 -2
- package/dist/function/functions.types.d.ts +3 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/middleware/index.d.ts +2 -2
- package/dist/middleware/index.js +2 -2
- package/dist/middleware/telemetry.d.ts +2 -2
- package/dist/middleware/telemetry.js +2 -2
- package/dist/middleware-runner.d.ts +15 -27
- package/dist/middleware-runner.js +25 -30
- package/dist/permissions.d.ts +15 -22
- package/dist/permissions.js +30 -25
- package/dist/pikku-request.js +1 -1
- package/dist/pikku-state.js +2 -3
- package/dist/services/ai-agent-runner-service.d.ts +1 -0
- package/dist/services/in-memory-queue-service.js +1 -1
- package/dist/services/in-memory-workflow-service.d.ts +15 -13
- package/dist/services/in-memory-workflow-service.js +31 -13
- package/dist/services/local-content.js +8 -1
- package/dist/services/user-session-service.d.ts +1 -1
- package/dist/testing/service-tests.js +24 -0
- package/dist/types/core.types.d.ts +4 -0
- package/dist/types/state.types.d.ts +2 -18
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
- package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
- package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
- package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
- package/dist/wirings/channel/channel-handler.js +1 -1
- package/dist/wirings/channel/channel-store.d.ts +13 -0
- package/dist/wirings/channel/channel.types.d.ts +3 -0
- package/dist/wirings/channel/local/local-channel-runner.js +23 -5
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
- package/dist/wirings/cli/cli-runner.js +24 -5
- package/dist/wirings/cli/command-parser.js +19 -4
- package/dist/wirings/http/http-runner.js +72 -36
- package/dist/wirings/http/http.types.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
- package/dist/wirings/http/web-request.js +32 -0
- package/dist/wirings/rpc/rpc-runner.js +13 -3
- package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
- package/dist/wirings/workflow/graph/graph-node.js +4 -0
- package/dist/wirings/workflow/graph/graph-runner.js +12 -10
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
- package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
- package/dist/wirings/workflow/workflow.types.d.ts +34 -0
- package/package.json +1 -1
- package/run-tests.sh +4 -1
- package/src/dev/hot-reload.test.ts +62 -11
- package/src/dev/hot-reload.ts +28 -7
- package/src/errors/error-handler.ts +8 -2
- package/src/errors/error.test.ts +2 -0
- package/src/function/function-runner.test.ts +500 -10
- package/src/function/functions.types.ts +3 -2
- package/src/index.ts +12 -2
- package/src/middleware/index.ts +11 -2
- package/src/middleware/telemetry.ts +2 -2
- package/src/middleware-runner.test.ts +16 -16
- package/src/middleware-runner.ts +42 -30
- package/src/permissions.test.ts +27 -24
- package/src/permissions.ts +41 -25
- package/src/pikku-request.test.ts +35 -0
- package/src/pikku-request.ts +1 -1
- package/src/pikku-state.ts +2 -3
- package/src/run-tests-script.test.ts +18 -0
- package/src/services/ai-agent-runner-service.ts +1 -0
- package/src/services/in-memory-queue-service.ts +1 -1
- package/src/services/in-memory-session-store.ts +1 -2
- package/src/services/in-memory-workflow-service.test.ts +33 -11
- package/src/services/in-memory-workflow-service.ts +49 -13
- package/src/services/local-content.test.ts +54 -0
- package/src/services/local-content.ts +12 -1
- package/src/services/typed-credential-service.ts +3 -3
- package/src/services/typed-secret-service.ts +3 -3
- package/src/services/typed-variables-service.ts +3 -3
- package/src/services/user-session-service.ts +4 -4
- package/src/testing/service-tests.ts +30 -0
- package/src/types/core.types.ts +6 -2
- package/src/types/state.types.ts +2 -13
- package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
- package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
- package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
- package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
- package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
- package/src/wirings/channel/channel-handler.test.ts +272 -0
- package/src/wirings/channel/channel-handler.ts +1 -1
- package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
- package/src/wirings/channel/channel-store.ts +19 -0
- package/src/wirings/channel/channel.types.ts +4 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
- package/src/wirings/channel/local/local-channel-runner.ts +41 -5
- package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
- package/src/wirings/cli/cli-runner.test.ts +255 -2
- package/src/wirings/cli/cli-runner.ts +31 -10
- package/src/wirings/cli/cli.types.ts +4 -8
- package/src/wirings/cli/command-parser.test.ts +83 -0
- package/src/wirings/cli/command-parser.ts +23 -4
- package/src/wirings/http/http-runner.test.ts +296 -1
- package/src/wirings/http/http-runner.ts +87 -57
- package/src/wirings/http/http.types.ts +11 -26
- package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
- package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
- package/src/wirings/http/web-request.test.ts +115 -0
- package/src/wirings/http/web-request.ts +43 -5
- package/src/wirings/mcp/mcp-runner.test.ts +367 -0
- package/src/wirings/queue/queue.types.ts +2 -5
- package/src/wirings/rpc/rpc-runner.test.ts +511 -0
- package/src/wirings/rpc/rpc-runner.ts +15 -3
- package/src/wirings/rpc/wire-addon.test.ts +57 -0
- package/src/wirings/workflow/graph/graph-node.ts +12 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
- package/src/wirings/workflow/graph/graph-runner.ts +28 -10
- package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
- package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
- package/src/wirings/workflow/workflow.types.ts +68 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,7 +2,15 @@ import { test, describe, beforeEach, afterEach } from 'node:test'
|
|
|
2
2
|
import * as assert from 'assert'
|
|
3
3
|
import { NotFoundError } from '../../errors/errors.js'
|
|
4
4
|
import type { JSONValue, CorePikkuMiddleware } from '../../types/core.types.js'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
addHTTPMiddleware,
|
|
7
|
+
addHTTPPermission,
|
|
8
|
+
createHTTPWire,
|
|
9
|
+
fetch,
|
|
10
|
+
fetchData,
|
|
11
|
+
pikkuFetch,
|
|
12
|
+
wireHTTP,
|
|
13
|
+
} from './http-runner.js'
|
|
6
14
|
import { pikkuState, resetPikkuState } from '../../pikku-state.js'
|
|
7
15
|
import {
|
|
8
16
|
PikkuMockRequest,
|
|
@@ -16,6 +24,62 @@ const sessionMiddleware: CorePikkuMiddleware = async (services, wire, next) => {
|
|
|
16
24
|
await next()
|
|
17
25
|
}
|
|
18
26
|
|
|
27
|
+
class TestRequest extends PikkuMockRequest {
|
|
28
|
+
private _headers: Record<string, string> = {}
|
|
29
|
+
private _data: unknown = {}
|
|
30
|
+
|
|
31
|
+
setHeaders(headers: Record<string, string>) {
|
|
32
|
+
this._headers = headers
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setData(data: unknown) {
|
|
36
|
+
this._data = data
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
headers(): Record<string, string> {
|
|
40
|
+
return this._headers
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
header(headerName: string): string | null {
|
|
44
|
+
return this._headers[headerName] ?? null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async data() {
|
|
48
|
+
return this._data
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class TestResponse extends PikkuMockResponse {
|
|
53
|
+
public headersMap = new Map<string, string | string[]>()
|
|
54
|
+
public jsonBody: unknown
|
|
55
|
+
public bufferBody: unknown
|
|
56
|
+
public mode: 'stream' | null = null
|
|
57
|
+
public closed = false
|
|
58
|
+
|
|
59
|
+
header(name: string, value: string | string[]): this {
|
|
60
|
+
this.headersMap.set(name, value)
|
|
61
|
+
return this
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
arrayBuffer(data: XMLHttpRequestBodyInit): this {
|
|
65
|
+
this.bufferBody = data
|
|
66
|
+
return this
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
json(data: unknown): this {
|
|
70
|
+
this.jsonBody = data
|
|
71
|
+
return this
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setMode(mode: 'stream') {
|
|
75
|
+
this.mode = mode
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
close() {
|
|
79
|
+
this.closed = true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
19
83
|
const setHTTPFunctionMap = (func: any) => {
|
|
20
84
|
pikkuState(null, 'function', 'meta', {
|
|
21
85
|
pikku_func_name: {
|
|
@@ -41,6 +105,35 @@ const setHTTPFunctionMap = (func: any) => {
|
|
|
41
105
|
addFunction('pikku_func_name', { func })
|
|
42
106
|
}
|
|
43
107
|
|
|
108
|
+
const setRouteMeta = (
|
|
109
|
+
route: string,
|
|
110
|
+
method:
|
|
111
|
+
| 'get'
|
|
112
|
+
| 'post'
|
|
113
|
+
| 'put'
|
|
114
|
+
| 'delete'
|
|
115
|
+
| 'patch'
|
|
116
|
+
| 'head'
|
|
117
|
+
| 'options' = 'get',
|
|
118
|
+
overrides: Record<string, unknown> = {}
|
|
119
|
+
) => {
|
|
120
|
+
const meta = pikkuState(null, 'http', 'meta')
|
|
121
|
+
meta[method][route] = {
|
|
122
|
+
pikkuFuncId: 'pikku_func_name',
|
|
123
|
+
route,
|
|
124
|
+
method,
|
|
125
|
+
...overrides,
|
|
126
|
+
} as any
|
|
127
|
+
pikkuState(null, 'function', 'meta')['pikku_func_name'] = {
|
|
128
|
+
pikkuFuncId: 'pikku_func_name',
|
|
129
|
+
inputSchemaName: null,
|
|
130
|
+
outputSchemaName: null,
|
|
131
|
+
sessionless: true,
|
|
132
|
+
permissions: undefined,
|
|
133
|
+
middleware: undefined,
|
|
134
|
+
} as any
|
|
135
|
+
}
|
|
136
|
+
|
|
44
137
|
describe('fetch', () => {
|
|
45
138
|
let request: any
|
|
46
139
|
let response: any
|
|
@@ -142,3 +235,205 @@ describe('fetch', () => {
|
|
|
142
235
|
)
|
|
143
236
|
})
|
|
144
237
|
})
|
|
238
|
+
|
|
239
|
+
describe('http-runner helpers', () => {
|
|
240
|
+
beforeEach(() => {
|
|
241
|
+
resetPikkuState()
|
|
242
|
+
httpRouter.reset()
|
|
243
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
244
|
+
logger: {
|
|
245
|
+
info: () => {},
|
|
246
|
+
warn: () => {},
|
|
247
|
+
error: () => {},
|
|
248
|
+
},
|
|
249
|
+
} as any)
|
|
250
|
+
pikkuState(null, 'package', 'factories', {
|
|
251
|
+
createWireServices: async () => ({}),
|
|
252
|
+
} as any)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test('addHTTPMiddleware and addHTTPPermission register route groups', () => {
|
|
256
|
+
const middleware = [sessionMiddleware]
|
|
257
|
+
const permissions = [async () => true]
|
|
258
|
+
|
|
259
|
+
assert.strictEqual(addHTTPMiddleware('/api/*', middleware), middleware)
|
|
260
|
+
assert.strictEqual(
|
|
261
|
+
addHTTPPermission('/api/*', permissions as any),
|
|
262
|
+
permissions
|
|
263
|
+
)
|
|
264
|
+
assert.strictEqual(
|
|
265
|
+
pikkuState(null, 'middleware', 'httpGroup')['/api/*'],
|
|
266
|
+
middleware
|
|
267
|
+
)
|
|
268
|
+
assert.strictEqual(
|
|
269
|
+
pikkuState(null, 'permissions', 'httpGroup')['/api/*'],
|
|
270
|
+
permissions
|
|
271
|
+
)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('createHTTPWire combines request and response when present', () => {
|
|
275
|
+
const req = new TestRequest('/x', 'get')
|
|
276
|
+
const res = new TestResponse()
|
|
277
|
+
|
|
278
|
+
assert.deepEqual(createHTTPWire(undefined, undefined), undefined)
|
|
279
|
+
assert.deepEqual(createHTTPWire(req, undefined), { request: req })
|
|
280
|
+
assert.deepEqual(createHTTPWire(undefined, res), { response: res })
|
|
281
|
+
assert.deepEqual(createHTTPWire(req, res), { request: req, response: res })
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('wireHTTP skips routes without metadata', () => {
|
|
285
|
+
const warnings: string[] = []
|
|
286
|
+
const originalWarn = console.warn
|
|
287
|
+
console.warn = (message: string) => {
|
|
288
|
+
warnings.push(message)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
wireHTTP({
|
|
293
|
+
route: '/missing',
|
|
294
|
+
method: 'get',
|
|
295
|
+
func: { func: async () => ({ ok: true }) },
|
|
296
|
+
})
|
|
297
|
+
} finally {
|
|
298
|
+
console.warn = originalWarn
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
assert.equal(
|
|
302
|
+
pikkuState(null, 'http', 'routes').get('get')?.has('/missing') ?? false,
|
|
303
|
+
false
|
|
304
|
+
)
|
|
305
|
+
assert.match(warnings[0] || '', /Skipping HTTP route 'GET \/missing'/)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('wireHTTP registers route functions when metadata exists', () => {
|
|
309
|
+
setRouteMeta('/registered')
|
|
310
|
+
|
|
311
|
+
wireHTTP({
|
|
312
|
+
route: '/registered',
|
|
313
|
+
method: 'get',
|
|
314
|
+
func: { func: async () => ({ ok: true }) },
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
httpRouter.initialize()
|
|
318
|
+
|
|
319
|
+
assert.equal(
|
|
320
|
+
pikkuState(null, 'http', 'routes').get('get')?.has('/registered'),
|
|
321
|
+
true
|
|
322
|
+
)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
test('fetchData handles OPTIONS preflight through global middleware', async () => {
|
|
326
|
+
const request = new TestRequest('/anything', 'options')
|
|
327
|
+
const response = new TestResponse()
|
|
328
|
+
const execution: string[] = []
|
|
329
|
+
|
|
330
|
+
addHTTPMiddleware('*', [
|
|
331
|
+
async (_services, _wire, next) => {
|
|
332
|
+
execution.push('global')
|
|
333
|
+
await next?.()
|
|
334
|
+
},
|
|
335
|
+
])
|
|
336
|
+
|
|
337
|
+
await fetchData(request, response)
|
|
338
|
+
|
|
339
|
+
assert.equal(response.statusCode, 204)
|
|
340
|
+
assert.equal(response.jsonBody, undefined)
|
|
341
|
+
assert.deepEqual(execution, ['global'])
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test('fetchData sets 204 when a route returns undefined', async () => {
|
|
345
|
+
setRouteMeta('/no-content')
|
|
346
|
+
addFunction('pikku_func_name', {
|
|
347
|
+
func: async () => undefined,
|
|
348
|
+
})
|
|
349
|
+
wireHTTP({
|
|
350
|
+
route: '/no-content',
|
|
351
|
+
method: 'get',
|
|
352
|
+
auth: false,
|
|
353
|
+
func: { func: async () => undefined },
|
|
354
|
+
})
|
|
355
|
+
httpRouter.initialize()
|
|
356
|
+
|
|
357
|
+
const request = new TestRequest('/no-content', 'get')
|
|
358
|
+
const response = new TestResponse()
|
|
359
|
+
|
|
360
|
+
await fetchData(request, response)
|
|
361
|
+
|
|
362
|
+
assert.equal(response.statusCode, 204)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('fetchData writes binary responses when returnsJSON is false', async () => {
|
|
366
|
+
setRouteMeta('/binary')
|
|
367
|
+
wireHTTP({
|
|
368
|
+
route: '/binary',
|
|
369
|
+
method: 'get',
|
|
370
|
+
auth: false,
|
|
371
|
+
returnsJSON: false,
|
|
372
|
+
func: { func: async () => 'raw-body' },
|
|
373
|
+
})
|
|
374
|
+
httpRouter.initialize()
|
|
375
|
+
|
|
376
|
+
const request = new TestRequest('/binary', 'get')
|
|
377
|
+
const response = new TestResponse()
|
|
378
|
+
|
|
379
|
+
await fetchData(request, response)
|
|
380
|
+
|
|
381
|
+
assert.equal(response.bufferBody, 'raw-body')
|
|
382
|
+
assert.equal(response.jsonBody, undefined)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
test('fetchData applies native Response results', async () => {
|
|
386
|
+
setRouteMeta('/native-response')
|
|
387
|
+
wireHTTP({
|
|
388
|
+
route: '/native-response',
|
|
389
|
+
method: 'get',
|
|
390
|
+
auth: false,
|
|
391
|
+
func: {
|
|
392
|
+
func: async () =>
|
|
393
|
+
new Response('hello', {
|
|
394
|
+
status: 202,
|
|
395
|
+
headers: { 'content-type': 'text/plain', 'x-test': 'ok' },
|
|
396
|
+
}),
|
|
397
|
+
},
|
|
398
|
+
})
|
|
399
|
+
httpRouter.initialize()
|
|
400
|
+
|
|
401
|
+
const result = await pikkuFetch(
|
|
402
|
+
new Request('https://example.com/native-response')
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
const response = result.toResponse()
|
|
406
|
+
assert.equal(response.status, 202)
|
|
407
|
+
assert.equal(await response.text(), 'hello')
|
|
408
|
+
assert.equal(response.headers.get('x-test'), 'ok')
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
test('fetchData configures SSE mode and streams through channel.send', async () => {
|
|
412
|
+
setRouteMeta('/sse', 'get', { sse: true })
|
|
413
|
+
wireHTTP({
|
|
414
|
+
route: '/sse',
|
|
415
|
+
method: 'get',
|
|
416
|
+
sse: true,
|
|
417
|
+
auth: false,
|
|
418
|
+
func: {
|
|
419
|
+
func: async (_services, _data, wire) => {
|
|
420
|
+
await wire.channel.send({ hello: 'world' })
|
|
421
|
+
wire.channel.close()
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
})
|
|
425
|
+
httpRouter.initialize()
|
|
426
|
+
|
|
427
|
+
const request = new TestRequest('/sse', 'get')
|
|
428
|
+
request.setData({ open: true })
|
|
429
|
+
const response = new TestResponse()
|
|
430
|
+
|
|
431
|
+
await fetchData(request, response)
|
|
432
|
+
|
|
433
|
+
assert.equal(response.mode, 'stream')
|
|
434
|
+
assert.equal(response.headersMap.get('Content-Type'), 'text/event-stream')
|
|
435
|
+
assert.equal(response.headersMap.get('Cache-Control'), 'no-cache')
|
|
436
|
+
assert.equal(response.closed, true)
|
|
437
|
+
assert.equal(response.bufferBody, JSON.stringify({ hello: 'world' }))
|
|
438
|
+
})
|
|
439
|
+
})
|
|
@@ -159,12 +159,10 @@ export const wireHTTP = <
|
|
|
159
159
|
Out,
|
|
160
160
|
Route extends string,
|
|
161
161
|
PikkuFunction extends CorePikkuFunction<In, Out> = CorePikkuFunction<In, Out>,
|
|
162
|
-
PikkuFunctionSessionless extends CorePikkuFunctionSessionless<
|
|
163
|
-
In,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
PikkuPermissionGroup extends
|
|
167
|
-
CorePikkuPermission<In> = CorePikkuPermission<In>,
|
|
162
|
+
PikkuFunctionSessionless extends CorePikkuFunctionSessionless<In, Out> =
|
|
163
|
+
CorePikkuFunctionSessionless<In, Out>,
|
|
164
|
+
PikkuPermissionGroup extends CorePikkuPermission<In> =
|
|
165
|
+
CorePikkuPermission<In>,
|
|
168
166
|
PikkuMiddleware extends CorePikkuMiddleware = CorePikkuMiddleware,
|
|
169
167
|
>(
|
|
170
168
|
httpWiring: CoreHTTPFunctionWiring<
|
|
@@ -360,8 +358,10 @@ const executeRoute = async (
|
|
|
360
358
|
response.setMode('stream')
|
|
361
359
|
response.header('Content-Type', 'text/event-stream')
|
|
362
360
|
response.header('Cache-Control', 'no-cache')
|
|
361
|
+
let sseState: unknown
|
|
362
|
+
const channelId = createWeakUID()
|
|
363
363
|
channel = {
|
|
364
|
-
channelId
|
|
364
|
+
channelId,
|
|
365
365
|
openingData: await data(),
|
|
366
366
|
send: (data: any) => {
|
|
367
367
|
response.arrayBuffer(isSerializable(data) ? JSON.stringify(data) : data)
|
|
@@ -374,6 +374,34 @@ const executeRoute = async (
|
|
|
374
374
|
response.close?.()
|
|
375
375
|
},
|
|
376
376
|
state: 'open',
|
|
377
|
+
setState: (s) => {
|
|
378
|
+
sseState = s
|
|
379
|
+
},
|
|
380
|
+
getState: () => sseState as any,
|
|
381
|
+
clearState: () => {
|
|
382
|
+
sseState = undefined
|
|
383
|
+
},
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Register the SSE channel with the eventHub (if present) so that
|
|
387
|
+
// eventHub.publish() can deliver messages to SSE subscribers.
|
|
388
|
+
// The channel handler wraps the SSE channel to match PikkuChannelHandler.
|
|
389
|
+
if (singletonServices.eventHub?.onChannelOpened) {
|
|
390
|
+
const channelRef = channel
|
|
391
|
+
const channelHandler = {
|
|
392
|
+
getChannel: () => channelRef,
|
|
393
|
+
send: (data: unknown, isBinary?: boolean) => {
|
|
394
|
+
if (isBinary) channelRef.sendBinary(data as any)
|
|
395
|
+
else channelRef.send(data)
|
|
396
|
+
},
|
|
397
|
+
sendBinary: (data: any) => channelRef.sendBinary(data),
|
|
398
|
+
}
|
|
399
|
+
singletonServices.eventHub.onChannelOpened(channelHandler)
|
|
400
|
+
const originalClose = channel.close
|
|
401
|
+
channel.close = () => {
|
|
402
|
+
singletonServices.eventHub.onChannelClosed(channelId)
|
|
403
|
+
originalClose()
|
|
404
|
+
}
|
|
377
405
|
}
|
|
378
406
|
}
|
|
379
407
|
|
|
@@ -387,27 +415,49 @@ const executeRoute = async (
|
|
|
387
415
|
hasSessionChanged: () => userSession.sessionChanged,
|
|
388
416
|
}
|
|
389
417
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
418
|
+
try {
|
|
419
|
+
result = await runPikkuFunc(
|
|
420
|
+
'http',
|
|
421
|
+
`${meta.method}:${meta.route}`,
|
|
422
|
+
meta.pikkuFuncId,
|
|
423
|
+
{
|
|
424
|
+
singletonServices,
|
|
425
|
+
createWireServices,
|
|
426
|
+
auth: route.auth !== false,
|
|
427
|
+
data,
|
|
428
|
+
inheritedMiddleware: meta.middleware,
|
|
429
|
+
wireMiddleware: route.middleware,
|
|
430
|
+
inheritedPermissions: meta.permissions,
|
|
431
|
+
wirePermissions: route.permissions,
|
|
432
|
+
coerceDataFromSchema: options.coerceDataFromSchema,
|
|
433
|
+
tags: route.tags,
|
|
434
|
+
wire,
|
|
435
|
+
sessionService: userSession,
|
|
436
|
+
packageName: meta.packageName,
|
|
437
|
+
}
|
|
438
|
+
)
|
|
439
|
+
} catch (e: any) {
|
|
440
|
+
if (matchedRoute.route.sse) {
|
|
441
|
+
singletonServices.logger.error(e instanceof Error ? e.message : e)
|
|
442
|
+
try {
|
|
443
|
+
const errorResponse = getErrorResponse(e)
|
|
444
|
+
http?.response?.arrayBuffer(
|
|
445
|
+
JSON.stringify({
|
|
446
|
+
type: 'error',
|
|
447
|
+
errorText: errorResponse?.message ?? 'Internal server error',
|
|
448
|
+
})
|
|
449
|
+
)
|
|
450
|
+
http?.response?.arrayBuffer(JSON.stringify({ type: 'done' }))
|
|
451
|
+
} catch {}
|
|
452
|
+
channel?.close()
|
|
453
|
+
return wireServices ? { result, wireServices } : { result }
|
|
408
454
|
}
|
|
409
|
-
|
|
410
|
-
|
|
455
|
+
throw e
|
|
456
|
+
}
|
|
457
|
+
if (matchedRoute.route.sse) {
|
|
458
|
+
// Flush headers after middleware has run so CORS/auth headers are included
|
|
459
|
+
http?.response?.flushHeaders?.()
|
|
460
|
+
} else {
|
|
411
461
|
if (result instanceof Response) {
|
|
412
462
|
await applyWebResponse(http!.response!, result)
|
|
413
463
|
} else if (result === undefined || result === null) {
|
|
@@ -567,36 +617,16 @@ export const fetchData = async <In, Out>(
|
|
|
567
617
|
|
|
568
618
|
return result
|
|
569
619
|
} catch (e: any) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
)
|
|
581
|
-
response.arrayBuffer(JSON.stringify({ type: 'done' }))
|
|
582
|
-
} catch (streamErr: any) {
|
|
583
|
-
scopedLogger.error(
|
|
584
|
-
`SSE error while sending error payload: ${streamErr instanceof Error ? streamErr.message : String(streamErr)}`
|
|
585
|
-
)
|
|
586
|
-
}
|
|
587
|
-
response.close?.()
|
|
588
|
-
} else {
|
|
589
|
-
handleHTTPError(
|
|
590
|
-
e,
|
|
591
|
-
http,
|
|
592
|
-
requestId,
|
|
593
|
-
scopedLogger,
|
|
594
|
-
logWarningsForStatusCodes,
|
|
595
|
-
respondWith404,
|
|
596
|
-
bubbleErrors,
|
|
597
|
-
exposeErrors
|
|
598
|
-
)
|
|
599
|
-
}
|
|
620
|
+
handleHTTPError(
|
|
621
|
+
e,
|
|
622
|
+
http,
|
|
623
|
+
requestId,
|
|
624
|
+
scopedLogger,
|
|
625
|
+
logWarningsForStatusCodes,
|
|
626
|
+
respondWith404,
|
|
627
|
+
bubbleErrors,
|
|
628
|
+
exposeErrors
|
|
629
|
+
)
|
|
600
630
|
} finally {
|
|
601
631
|
// Clean up any session-specific services created during processing
|
|
602
632
|
if (wireServices) {
|
|
@@ -116,13 +116,8 @@ export type CoreHTTPFunctionWiring<
|
|
|
116
116
|
In,
|
|
117
117
|
Out,
|
|
118
118
|
R extends string,
|
|
119
|
-
PikkuFunction extends CorePikkuFunction<
|
|
120
|
-
In,
|
|
121
|
-
Out,
|
|
122
|
-
any,
|
|
123
|
-
any,
|
|
124
|
-
any
|
|
125
|
-
> = CorePikkuFunction<In, Out>,
|
|
119
|
+
PikkuFunction extends CorePikkuFunction<In, Out, any, any, any> =
|
|
120
|
+
CorePikkuFunction<In, Out>,
|
|
126
121
|
PikkuFunctionSessionless extends CorePikkuFunctionSessionless<
|
|
127
122
|
In,
|
|
128
123
|
Out,
|
|
@@ -130,15 +125,10 @@ export type CoreHTTPFunctionWiring<
|
|
|
130
125
|
any,
|
|
131
126
|
any
|
|
132
127
|
> = CorePikkuFunctionSessionless<In, Out>,
|
|
133
|
-
PikkuPermission extends CorePikkuPermission<
|
|
134
|
-
In,
|
|
135
|
-
|
|
136
|
-
any
|
|
137
|
-
> = CorePikkuPermission<In, any, any>,
|
|
138
|
-
PikkuMiddleware extends CorePikkuMiddleware<
|
|
139
|
-
any,
|
|
140
|
-
any
|
|
141
|
-
> = CorePikkuMiddleware<any>,
|
|
128
|
+
PikkuPermission extends CorePikkuPermission<In, any, any> =
|
|
129
|
+
CorePikkuPermission<In, any, any>,
|
|
130
|
+
PikkuMiddleware extends CorePikkuMiddleware<any, any> =
|
|
131
|
+
CorePikkuMiddleware<any>,
|
|
142
132
|
> =
|
|
143
133
|
| (CoreHTTPFunction & {
|
|
144
134
|
route: R
|
|
@@ -290,6 +280,7 @@ export interface PikkuHTTPResponse<Out = unknown> {
|
|
|
290
280
|
redirect(location: string, status?: number): this
|
|
291
281
|
close?: () => void
|
|
292
282
|
setMode?: (mode: 'stream') => void
|
|
283
|
+
flushHeaders?: () => void
|
|
293
284
|
}
|
|
294
285
|
|
|
295
286
|
/**
|
|
@@ -301,11 +292,8 @@ export type HTTPRouteConfig<
|
|
|
301
292
|
| CorePikkuFunctionSessionless<any, any, any, any, any> =
|
|
302
293
|
| CorePikkuFunction<any, any, any, any, any>
|
|
303
294
|
| CorePikkuFunctionSessionless<any, any, any, any, any>,
|
|
304
|
-
PikkuPermission extends CorePikkuPermission<
|
|
305
|
-
any
|
|
306
|
-
any,
|
|
307
|
-
any
|
|
308
|
-
> = CorePikkuPermission<any>,
|
|
295
|
+
PikkuPermission extends CorePikkuPermission<any, any, any> =
|
|
296
|
+
CorePikkuPermission<any>,
|
|
309
297
|
PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<
|
|
310
298
|
any,
|
|
311
299
|
any
|
|
@@ -331,11 +319,8 @@ export type HTTPRouteConfig<
|
|
|
331
319
|
* Group-level configuration applied to all routes
|
|
332
320
|
*/
|
|
333
321
|
export type HTTPRoutesGroupConfig<
|
|
334
|
-
PikkuPermission extends CorePikkuPermission<
|
|
335
|
-
any
|
|
336
|
-
any,
|
|
337
|
-
any
|
|
338
|
-
> = CorePikkuPermission<any>,
|
|
322
|
+
PikkuPermission extends CorePikkuPermission<any, any, any> =
|
|
323
|
+
CorePikkuPermission<any>,
|
|
339
324
|
PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<
|
|
340
325
|
any,
|
|
341
326
|
any
|
|
@@ -8,9 +8,9 @@ import { UnprocessableContentError } from '../../errors/errors.js'
|
|
|
8
8
|
* @template In - The type of the request body.
|
|
9
9
|
* @group RequestResponse
|
|
10
10
|
*/
|
|
11
|
-
export class PikkuFetchHTTPRequest<
|
|
12
|
-
|
|
13
|
-
{
|
|
11
|
+
export class PikkuFetchHTTPRequest<
|
|
12
|
+
In = unknown,
|
|
13
|
+
> implements PikkuHTTPRequest<In> {
|
|
14
14
|
#cookies: Partial<Record<string, string>> | undefined
|
|
15
15
|
#params: Partial<Record<string, string | string[]>> = {}
|
|
16
16
|
#url: URL
|
|
@@ -105,7 +105,11 @@ export class PikkuFetchHTTPRequest<In = unknown>
|
|
|
105
105
|
const merged: Record<string, unknown> = {}
|
|
106
106
|
for (const part of parts) {
|
|
107
107
|
for (const [key, value] of Object.entries(part)) {
|
|
108
|
-
if (
|
|
108
|
+
if (
|
|
109
|
+
key === '__proto__' ||
|
|
110
|
+
key === 'constructor' ||
|
|
111
|
+
key === 'prototype'
|
|
112
|
+
) {
|
|
109
113
|
continue
|
|
110
114
|
}
|
|
111
115
|
if (key in merged && !valuesAreEquivalent(merged[key], value)) {
|
|
@@ -79,4 +79,45 @@ describe('PikkuFetchHTTPResponse', () => {
|
|
|
79
79
|
assert.strictEqual(res.status, 301)
|
|
80
80
|
assert.strictEqual(res.headers.get('Location'), '/login')
|
|
81
81
|
})
|
|
82
|
+
|
|
83
|
+
test('redirect rejects invalid redirect locations', () => {
|
|
84
|
+
assert.throws(
|
|
85
|
+
() => new PikkuFetchHTTPResponse().redirect('login'),
|
|
86
|
+
/Invalid redirect location/
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('send stores raw body content', async () => {
|
|
91
|
+
const res = new PikkuFetchHTTPResponse().send('plain-body').toResponse()
|
|
92
|
+
|
|
93
|
+
assert.strictEqual(await res.text(), 'plain-body')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('stream mode writes json/text/html/arrayBuffer and close completes the stream', async () => {
|
|
97
|
+
const res = new PikkuFetchHTTPResponse()
|
|
98
|
+
res.setMode('stream')
|
|
99
|
+
res.json({ hello: 'world' })
|
|
100
|
+
res.text('plain')
|
|
101
|
+
res.html('<p>html</p>')
|
|
102
|
+
res.arrayBuffer('bytes')
|
|
103
|
+
res.close()
|
|
104
|
+
|
|
105
|
+
const response = res.toResponse()
|
|
106
|
+
const text = await response.text()
|
|
107
|
+
|
|
108
|
+
assert.ok(text.startsWith(':\n\n'))
|
|
109
|
+
assert.ok(text.includes('data: {"hello":"world"}\n\n'))
|
|
110
|
+
assert.ok(text.includes('data: plain\n\n'))
|
|
111
|
+
assert.ok(text.includes('data: <p>html</p>\n\n'))
|
|
112
|
+
assert.ok(text.includes('data: bytes\n\n'))
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('toResponse preserves the instance status over response init args', () => {
|
|
116
|
+
const res = new PikkuFetchHTTPResponse()
|
|
117
|
+
.status(201)
|
|
118
|
+
.json({ ok: true })
|
|
119
|
+
.toResponse({ status: 202 })
|
|
120
|
+
|
|
121
|
+
assert.strictEqual(res.status, 201)
|
|
122
|
+
})
|
|
82
123
|
})
|