@pikku/core 0.12.4 → 0.12.5
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 -1
- package/dist/dev/hot-reload.d.ts +10 -0
- package/dist/dev/hot-reload.js +158 -0
- package/dist/middleware-runner.d.ts +1 -0
- package/dist/middleware-runner.js +5 -0
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.js +5 -0
- package/dist/services/in-memory-workflow-service.js +7 -11
- package/dist/wirings/ai-agent/ai-agent-prepare.js +16 -1
- package/dist/wirings/channel/channel-middleware-runner.d.ts +1 -0
- package/dist/wirings/channel/channel-middleware-runner.js +5 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +7 -8
- package/package.json +3 -2
- package/src/crypto-utils.test.ts +214 -0
- package/src/crypto-utils.ts +213 -0
- package/src/dev/hot-reload.test.ts +484 -0
- package/src/dev/hot-reload.ts +212 -0
- package/src/errors/error-handler.ts +62 -0
- package/src/errors/error.test.ts +195 -0
- package/src/errors/errors.ts +374 -0
- package/src/errors/index.ts +2 -0
- package/src/factory-functions.test.ts +109 -0
- package/src/function/function-runner.test.ts +536 -0
- package/src/function/function-runner.ts +365 -0
- package/src/function/functions.types.ts +304 -0
- package/src/function/index.ts +5 -0
- package/src/handle-error.test.ts +424 -0
- package/src/handle-error.ts +69 -0
- package/src/index.ts +118 -0
- package/src/internal.ts +6 -0
- package/src/middleware/auth-apikey.test.ts +363 -0
- package/src/middleware/auth-apikey.ts +47 -0
- package/src/middleware/auth-bearer.test.ts +450 -0
- package/src/middleware/auth-bearer.ts +72 -0
- package/src/middleware/auth-cookie.test.ts +528 -0
- package/src/middleware/auth-cookie.ts +80 -0
- package/src/middleware/cors.test.ts +424 -0
- package/src/middleware/cors.ts +104 -0
- package/src/middleware/index.ts +5 -0
- package/src/middleware/remote-auth.test.ts +488 -0
- package/src/middleware/remote-auth.ts +68 -0
- package/src/middleware/timeout.ts +15 -0
- package/src/middleware-runner.test.ts +418 -0
- package/src/middleware-runner.ts +240 -0
- package/src/permissions.test.ts +434 -0
- package/src/permissions.ts +327 -0
- package/src/pikku-request.ts +23 -0
- package/src/pikku-response.ts +5 -0
- package/src/pikku-state.test.ts +224 -0
- package/src/pikku-state.ts +216 -0
- package/src/run-tests-script.test.ts +49 -0
- package/src/schema.test.ts +249 -0
- package/src/schema.ts +151 -0
- package/src/services/ai-agent-runner-service.ts +41 -0
- package/src/services/ai-run-state-service.ts +20 -0
- package/src/services/ai-storage-service.ts +39 -0
- package/src/services/content-service.ts +76 -0
- package/src/services/deployment-service.ts +22 -0
- package/src/services/gateway-service.ts +20 -0
- package/src/services/gopass-secrets.ts +98 -0
- package/src/services/in-memory-ai-run-state-service.ts +73 -0
- package/src/services/in-memory-trigger-service.ts +64 -0
- package/src/services/in-memory-workflow-service.test.ts +351 -0
- package/src/services/in-memory-workflow-service.ts +502 -0
- package/src/services/index.ts +56 -0
- package/src/services/jwt-service.ts +30 -0
- package/src/services/local-content.ts +120 -0
- package/src/services/local-gateway-service.ts +62 -0
- package/src/services/local-secrets.test.ts +80 -0
- package/src/services/local-secrets.ts +94 -0
- package/src/services/local-variables.test.ts +61 -0
- package/src/services/local-variables.ts +39 -0
- package/src/services/logger-console.test.ts +118 -0
- package/src/services/logger-console.ts +98 -0
- package/src/services/logger.ts +57 -0
- package/src/services/scheduler-service.ts +86 -0
- package/src/services/schema-service.ts +33 -0
- package/src/services/scoped-secret-service.test.ts +74 -0
- package/src/services/scoped-secret-service.ts +41 -0
- package/src/services/secret-service.ts +38 -0
- package/src/services/trigger-service.ts +17 -0
- package/src/services/typed-secret-service.test.ts +93 -0
- package/src/services/typed-secret-service.ts +70 -0
- package/src/services/typed-variables-service.test.ts +73 -0
- package/src/services/typed-variables-service.ts +81 -0
- package/src/services/user-session-service.test.ts +113 -0
- package/src/services/user-session-service.ts +86 -0
- package/src/services/variables-service.ts +11 -0
- package/src/services/workflow-service.ts +95 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/service-tests.ts +873 -0
- package/src/time-utils.test.ts +56 -0
- package/src/time-utils.ts +107 -0
- package/src/types/core.types.ts +478 -0
- package/src/types/state.types.ts +188 -0
- package/src/utils/hash.test.ts +68 -0
- package/src/utils/hash.ts +26 -0
- package/src/utils.test.ts +350 -0
- package/src/utils.ts +129 -0
- package/src/version.test.ts +80 -0
- package/src/version.ts +25 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
- package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
- package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +310 -0
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
- package/src/wirings/ai-agent/ai-agent-model-config.ts +43 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +659 -0
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +37 -0
- package/src/wirings/ai-agent/ai-agent-registry.ts +82 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +319 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +773 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +859 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +1153 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +382 -0
- package/src/wirings/ai-agent/index.ts +37 -0
- package/src/wirings/channel/channel-common.ts +90 -0
- package/src/wirings/channel/channel-handler.ts +250 -0
- package/src/wirings/channel/channel-middleware-runner.ts +126 -0
- package/src/wirings/channel/channel-runner.ts +166 -0
- package/src/wirings/channel/channel-store.ts +29 -0
- package/src/wirings/channel/channel.types.ts +172 -0
- package/src/wirings/channel/define-channel-routes.ts +25 -0
- package/src/wirings/channel/eventhub-service.ts +34 -0
- package/src/wirings/channel/eventhub-store.ts +13 -0
- package/src/wirings/channel/index.ts +24 -0
- package/src/wirings/channel/local/index.ts +3 -0
- package/src/wirings/channel/local/local-channel-handler.ts +81 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +215 -0
- package/src/wirings/channel/local/local-channel-runner.ts +210 -0
- package/src/wirings/channel/local/local-eventhub-service.test.ts +95 -0
- package/src/wirings/channel/local/local-eventhub-service.ts +101 -0
- package/src/wirings/channel/log-channels.ts +20 -0
- package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +54 -0
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +45 -0
- package/src/wirings/channel/serverless/index.ts +5 -0
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +289 -0
- package/src/wirings/cli/channel/cli-channel-runner.ts +136 -0
- package/src/wirings/cli/channel/index.ts +1 -0
- package/src/wirings/cli/cli-runner.test.ts +403 -0
- package/src/wirings/cli/cli-runner.ts +529 -0
- package/src/wirings/cli/cli.types.ts +358 -0
- package/src/wirings/cli/command-parser.test.ts +445 -0
- package/src/wirings/cli/command-parser.ts +504 -0
- package/src/wirings/cli/define-cli-commands.ts +24 -0
- package/src/wirings/cli/index.ts +19 -0
- package/src/wirings/gateway/gateway-runner.test.ts +474 -0
- package/src/wirings/gateway/gateway-runner.ts +411 -0
- package/src/wirings/gateway/gateway.types.ts +149 -0
- package/src/wirings/gateway/index.ts +13 -0
- package/src/wirings/http/http-routes.test.ts +322 -0
- package/src/wirings/http/http-routes.ts +197 -0
- package/src/wirings/http/http-runner.test.ts +144 -0
- package/src/wirings/http/http-runner.ts +588 -0
- package/src/wirings/http/http.types.ts +369 -0
- package/src/wirings/http/index.ts +28 -0
- package/src/wirings/http/log-http-routes.ts +22 -0
- package/src/wirings/http/pikku-fetch-http-request.test.ts +237 -0
- package/src/wirings/http/pikku-fetch-http-request.ts +170 -0
- package/src/wirings/http/pikku-fetch-http-response.test.ts +82 -0
- package/src/wirings/http/pikku-fetch-http-response.ts +147 -0
- package/src/wirings/http/routers/http-router.ts +13 -0
- package/src/wirings/http/routers/path-to-regex.test.ts +319 -0
- package/src/wirings/http/routers/path-to-regex.ts +126 -0
- package/src/wirings/http/web-request.test.ts +236 -0
- package/src/wirings/http/web-request.ts +104 -0
- package/src/wirings/mcp/index.ts +27 -0
- package/src/wirings/mcp/mcp-endpoint-registry.test.ts +389 -0
- package/src/wirings/mcp/mcp-endpoint-registry.ts +159 -0
- package/src/wirings/mcp/mcp-runner.ts +323 -0
- package/src/wirings/mcp/mcp.types.ts +216 -0
- package/src/wirings/node/index.ts +2 -0
- package/src/wirings/node/node.types.ts +23 -0
- package/src/wirings/oauth2/index.ts +3 -0
- package/src/wirings/oauth2/oauth2-client.test.ts +929 -0
- package/src/wirings/oauth2/oauth2-client.ts +335 -0
- package/src/wirings/oauth2/oauth2.types.ts +69 -0
- package/src/wirings/oauth2/wire-oauth2-credential.ts +23 -0
- package/src/wirings/queue/index.ts +31 -0
- package/src/wirings/queue/queue-runner.test.ts +710 -0
- package/src/wirings/queue/queue-runner.ts +192 -0
- package/src/wirings/queue/queue.types.ts +189 -0
- package/src/wirings/queue/register-queue-helper.ts +60 -0
- package/src/wirings/queue/validate-worker-config.test.ts +108 -0
- package/src/wirings/queue/validate-worker-config.ts +116 -0
- package/src/wirings/rpc/index.ts +4 -0
- package/src/wirings/rpc/rpc-runner.ts +460 -0
- package/src/wirings/rpc/rpc-types.ts +56 -0
- package/src/wirings/rpc/wire-addon.ts +21 -0
- package/src/wirings/scheduler/index.ts +11 -0
- package/src/wirings/scheduler/log-schedulers.ts +20 -0
- package/src/wirings/scheduler/scheduler-runner.test.ts +660 -0
- package/src/wirings/scheduler/scheduler-runner.ts +133 -0
- package/src/wirings/scheduler/scheduler.types.ts +53 -0
- package/src/wirings/secret/index.ts +9 -0
- package/src/wirings/secret/secret.types.ts +32 -0
- package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
- package/src/wirings/secret/validate-secret-definitions.ts +82 -0
- package/src/wirings/trigger/index.ts +10 -0
- package/src/wirings/trigger/pikku-trigger-service.ts +112 -0
- package/src/wirings/trigger/trigger-runner.test.ts +79 -0
- package/src/wirings/trigger/trigger-runner.ts +135 -0
- package/src/wirings/trigger/trigger.types.ts +178 -0
- package/src/wirings/variable/index.ts +8 -0
- package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
- package/src/wirings/variable/validate-variable-definitions.ts +69 -0
- package/src/wirings/variable/variable.types.ts +22 -0
- package/src/wirings/workflow/dsl/index.ts +31 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +320 -0
- package/src/wirings/workflow/dsl/workflow-runner.ts +28 -0
- package/src/wirings/workflow/graph/graph-node.ts +222 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +487 -0
- package/src/wirings/workflow/graph/graph-runner.ts +816 -0
- package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
- package/src/wirings/workflow/graph/graph-validation.ts +237 -0
- package/src/wirings/workflow/graph/index.ts +19 -0
- package/src/wirings/workflow/graph/template.test.ts +49 -0
- package/src/wirings/workflow/graph/template.ts +42 -0
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +29 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +104 -0
- package/src/wirings/workflow/index.ts +82 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +200 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +1179 -0
- package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
- package/src/wirings/workflow/workflow-helpers.ts +79 -0
- package/src/wirings/workflow/workflow.types.ts +274 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/lcov.info +0 -18891
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { parse as parseQuery } from 'picoquery'
|
|
2
|
+
import { parse as parseCookie } from 'cookie'
|
|
3
|
+
import type { HTTPMethod, PikkuHTTPRequest, PikkuQuery } from './http.types.js'
|
|
4
|
+
import { UnprocessableContentError } from '../../errors/errors.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Abstract class representing a pikku request.
|
|
8
|
+
* @template In - The type of the request body.
|
|
9
|
+
* @group RequestResponse
|
|
10
|
+
*/
|
|
11
|
+
export class PikkuFetchHTTPRequest<In = unknown>
|
|
12
|
+
implements PikkuHTTPRequest<In>
|
|
13
|
+
{
|
|
14
|
+
#cookies: Partial<Record<string, string>> | undefined
|
|
15
|
+
#params: Partial<Record<string, string | string[]>> = {}
|
|
16
|
+
#url: URL
|
|
17
|
+
|
|
18
|
+
constructor(private request: Request) {
|
|
19
|
+
this.#url = new URL(request.url)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public method(): HTTPMethod {
|
|
23
|
+
return this.request.method.toLowerCase() as HTTPMethod
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public path(): string {
|
|
27
|
+
return this.#url.pathname
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Retrieves the request body.
|
|
32
|
+
* @returns A promise that resolves to the request body.
|
|
33
|
+
*/
|
|
34
|
+
public json(): Promise<In> {
|
|
35
|
+
return this.request.json()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Retrieves the raw request body as a Buffer.
|
|
40
|
+
* @returns A promise that resolves to the raw request body.
|
|
41
|
+
*/
|
|
42
|
+
public arrayBuffer(): Promise<ArrayBuffer> {
|
|
43
|
+
return this.request.arrayBuffer()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public headers(): Record<string, string> {
|
|
47
|
+
return Object.fromEntries(this.request.headers.entries())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Retrieves the value of a specific header.
|
|
52
|
+
* @param headerName - The name of the header to retrieve.
|
|
53
|
+
* @returns The value of the header, or undefined if the header is not found.
|
|
54
|
+
*/
|
|
55
|
+
public header(headerName: string): string | null {
|
|
56
|
+
return this.request.headers.get(headerName.toLowerCase())
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Retrieves the cookies from the request.
|
|
61
|
+
* @returns An object containing the cookies.
|
|
62
|
+
*/
|
|
63
|
+
public cookie(cookieName: string): string | null {
|
|
64
|
+
if (this.#cookies?.[cookieName]) {
|
|
65
|
+
return this.#cookies[cookieName]
|
|
66
|
+
}
|
|
67
|
+
const cookieHeader = this.header('cookie')
|
|
68
|
+
this.#cookies = cookieHeader ? parseCookie(cookieHeader) : {}
|
|
69
|
+
return this.#cookies[cookieName] || null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Retrieves the request parameters.
|
|
74
|
+
* @returns An object containing the request parameters.
|
|
75
|
+
*/
|
|
76
|
+
public params(): Partial<Record<string, string | string[]>> {
|
|
77
|
+
return this.#params
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Sets the request parameters.
|
|
82
|
+
* @param params - An object containing the request parameters to set.
|
|
83
|
+
*/
|
|
84
|
+
public setParams(
|
|
85
|
+
params: Record<string, string | string[] | undefined>
|
|
86
|
+
): void {
|
|
87
|
+
this.#params = params
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Retrieves the query parameters from the request.
|
|
92
|
+
* @returns An object containing the query parameters.
|
|
93
|
+
*/
|
|
94
|
+
public query(): PikkuQuery {
|
|
95
|
+
return parseQuery(this.#url.searchParams.toString()) as PikkuQuery
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Retrieves the combined data from the request, including parameters, query, and body.
|
|
100
|
+
* @returns A promise that resolves to an object containing the combined data.
|
|
101
|
+
*/
|
|
102
|
+
public async data(): Promise<In> {
|
|
103
|
+
const body = await this.body()
|
|
104
|
+
const parts = [this.params(), this.query(), body]
|
|
105
|
+
const merged: Record<string, unknown> = {}
|
|
106
|
+
for (const part of parts) {
|
|
107
|
+
for (const [key, value] of Object.entries(part)) {
|
|
108
|
+
if (key in merged && !valuesAreEquivalent(merged[key], value)) {
|
|
109
|
+
throw new UnprocessableContentError(
|
|
110
|
+
`Conflicting values for key "${key}": "${merged[key]}" vs "${value}"`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
merged[key] ??= value
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return merged as In
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private async body(): Promise<any> {
|
|
120
|
+
const noBodyMethods: HTTPMethod[] = ['get', 'head', 'options', 'delete']
|
|
121
|
+
if (noBodyMethods.includes(this.method())) {
|
|
122
|
+
return {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let body: any = {}
|
|
126
|
+
const contentType = this.header('content-type') || ''
|
|
127
|
+
try {
|
|
128
|
+
if (contentType.includes('application/json')) {
|
|
129
|
+
const parsed = await this.json()
|
|
130
|
+
body =
|
|
131
|
+
typeof parsed === 'object' &&
|
|
132
|
+
parsed !== null &&
|
|
133
|
+
!Array.isArray(parsed)
|
|
134
|
+
? parsed
|
|
135
|
+
: { data: parsed }
|
|
136
|
+
} else if (contentType.includes('text/')) {
|
|
137
|
+
const text = await this.request.text()
|
|
138
|
+
body = { data: text }
|
|
139
|
+
} else if (contentType.includes('application/octet-stream')) {
|
|
140
|
+
const buffer = await this.request.arrayBuffer()
|
|
141
|
+
body = { data: buffer }
|
|
142
|
+
} else if (contentType === 'application/x-www-form-urlencoded') {
|
|
143
|
+
const text = await this.request.text()
|
|
144
|
+
body = Object.fromEntries(new URLSearchParams(text))
|
|
145
|
+
} else {
|
|
146
|
+
throw new UnprocessableContentError(
|
|
147
|
+
`Unsupported content type ${contentType}`
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
} catch (e) {
|
|
151
|
+
throw new UnprocessableContentError(`Error parsing body: ${e}`)
|
|
152
|
+
}
|
|
153
|
+
return body
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function valuesAreEquivalent(a: unknown, b: unknown): boolean {
|
|
158
|
+
return coerce(a) === coerce(b)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function coerce(value: unknown): string | number | boolean {
|
|
162
|
+
if (typeof value === 'boolean' || typeof value === 'number') return value
|
|
163
|
+
if (typeof value === 'string') {
|
|
164
|
+
if (value === 'true') return true
|
|
165
|
+
if (value === 'false') return false
|
|
166
|
+
const num = Number(value)
|
|
167
|
+
return isNaN(num) ? value : num
|
|
168
|
+
}
|
|
169
|
+
return value as any
|
|
170
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js' // Adjust path if needed
|
|
4
|
+
|
|
5
|
+
describe('PikkuFetchHTTPResponse', () => {
|
|
6
|
+
test('sets status code', () => {
|
|
7
|
+
const res = new PikkuFetchHTTPResponse().status(404).toResponse()
|
|
8
|
+
assert.strictEqual(res.status, 404)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('sets headers', async () => {
|
|
12
|
+
const res = new PikkuFetchHTTPResponse()
|
|
13
|
+
.header('X-Test', 'value')
|
|
14
|
+
.header('X-Multi', ['a', 'b'])
|
|
15
|
+
.toResponse()
|
|
16
|
+
|
|
17
|
+
assert.strictEqual(res.headers.get('X-Test'), 'value')
|
|
18
|
+
const multi = res.headers.get('X-Multi')?.split(', ')
|
|
19
|
+
assert.deepEqual(multi, ['a', 'b'])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('sets cookies', () => {
|
|
23
|
+
const res = new PikkuFetchHTTPResponse()
|
|
24
|
+
.cookie('token', 'abc123', { httpOnly: true, path: '/' })
|
|
25
|
+
.cookie('other', 'val', { maxAge: 3600 })
|
|
26
|
+
.toResponse()
|
|
27
|
+
|
|
28
|
+
const setCookieHeader = res.headers.get('Set-Cookie')
|
|
29
|
+
assert.ok(setCookieHeader)
|
|
30
|
+
|
|
31
|
+
const cookies = setCookieHeader!.split(', ')
|
|
32
|
+
assert.ok(cookies.some((c) => c.startsWith('token=abc123')))
|
|
33
|
+
assert.ok(cookies.some((c) => c.startsWith('other=val')))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('json sets correct content type and body', async () => {
|
|
37
|
+
const res = new PikkuFetchHTTPResponse()
|
|
38
|
+
.json({ hello: 'world' })
|
|
39
|
+
.toResponse()
|
|
40
|
+
|
|
41
|
+
assert.strictEqual(res.headers.get('Content-Type'), 'application/json')
|
|
42
|
+
const body = await res.json()
|
|
43
|
+
assert.deepEqual(body, { hello: 'world' })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('text sets correct content type and body', async () => {
|
|
47
|
+
const res = new PikkuFetchHTTPResponse().text('hello').toResponse()
|
|
48
|
+
|
|
49
|
+
assert.strictEqual(res.headers.get('Content-Type'), 'text/plain')
|
|
50
|
+
const text = await res.text()
|
|
51
|
+
assert.strictEqual(text, 'hello')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('html sets correct content type and body', async () => {
|
|
55
|
+
const res = new PikkuFetchHTTPResponse().html('<p>hi</p>').toResponse()
|
|
56
|
+
|
|
57
|
+
assert.strictEqual(res.headers.get('Content-Type'), 'text/html')
|
|
58
|
+
const text = await res.text()
|
|
59
|
+
assert.strictEqual(text, '<p>hi</p>')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('arrayBuffer sets correct content type', async () => {
|
|
63
|
+
const buffer = new Uint8Array([1, 2, 3]).buffer
|
|
64
|
+
const res = new PikkuFetchHTTPResponse().arrayBuffer(buffer).toResponse()
|
|
65
|
+
|
|
66
|
+
assert.strictEqual(
|
|
67
|
+
res.headers.get('Content-Type'),
|
|
68
|
+
'application/octet-stream'
|
|
69
|
+
)
|
|
70
|
+
const body = await res.arrayBuffer()
|
|
71
|
+
assert.deepEqual(new Uint8Array(body), new Uint8Array([1, 2, 3]))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('redirect sets Location header and status', () => {
|
|
75
|
+
const res = new PikkuFetchHTTPResponse()
|
|
76
|
+
.redirect('/login', 301)
|
|
77
|
+
.toResponse()
|
|
78
|
+
|
|
79
|
+
assert.strictEqual(res.status, 301)
|
|
80
|
+
assert.strictEqual(res.headers.get('Location'), '/login')
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { PikkuHTTPResponse } from './http.types.js'
|
|
2
|
+
import type { SerializeOptions as CookieSerializeOptions } from 'cookie'
|
|
3
|
+
import { serialize as serializeCookie } from 'cookie'
|
|
4
|
+
|
|
5
|
+
export class PikkuFetchHTTPResponse implements PikkuHTTPResponse {
|
|
6
|
+
#statusCode: number = 200
|
|
7
|
+
#headers = new Headers()
|
|
8
|
+
#cookies = new Map<string, { value: string; flags: CookieSerializeOptions }>()
|
|
9
|
+
|
|
10
|
+
#body: BodyInit | null = null
|
|
11
|
+
#responseMode: 'stream' | null = null
|
|
12
|
+
#send: ((data: string) => void) | null = null
|
|
13
|
+
#close: (() => void) | null = null
|
|
14
|
+
|
|
15
|
+
public setMode(mode: 'stream') {
|
|
16
|
+
this.#responseMode = 'stream'
|
|
17
|
+
if (mode === 'stream') {
|
|
18
|
+
this.#body = this.createStream()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public status(code: number): this {
|
|
23
|
+
this.#statusCode = code
|
|
24
|
+
return this
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public cookie(
|
|
28
|
+
name: string,
|
|
29
|
+
value: string,
|
|
30
|
+
flags: CookieSerializeOptions
|
|
31
|
+
): this {
|
|
32
|
+
this.#cookies.set(name, { value, flags })
|
|
33
|
+
return this
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public header(name: string, value: string | string[]): this {
|
|
37
|
+
this.#headers.delete(name)
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
value.forEach((v) => this.#headers.append(name, v))
|
|
40
|
+
} else {
|
|
41
|
+
this.#headers.set(name, value)
|
|
42
|
+
}
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public arrayBuffer(data: XMLHttpRequestBodyInit): this {
|
|
47
|
+
if (this.#responseMode === 'stream') {
|
|
48
|
+
this.#send!(data as string)
|
|
49
|
+
} else {
|
|
50
|
+
this.#body = data
|
|
51
|
+
this.header('Content-Type', 'application/octet-stream')
|
|
52
|
+
}
|
|
53
|
+
return this
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public json(data: unknown): this {
|
|
57
|
+
if (this.#responseMode === 'stream') {
|
|
58
|
+
this.#send!(JSON.stringify(data))
|
|
59
|
+
} else {
|
|
60
|
+
this.#body = JSON.stringify(data)
|
|
61
|
+
this.header('Content-Type', 'application/json')
|
|
62
|
+
}
|
|
63
|
+
return this
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public text(content: string): this {
|
|
67
|
+
if (this.#responseMode === 'stream') {
|
|
68
|
+
this.#send!(content)
|
|
69
|
+
} else {
|
|
70
|
+
this.#body = content
|
|
71
|
+
this.header('Content-Type', 'text/plain')
|
|
72
|
+
}
|
|
73
|
+
return this
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public html(content: string): this {
|
|
77
|
+
if (this.#responseMode === 'stream') {
|
|
78
|
+
this.#send!(content)
|
|
79
|
+
} else {
|
|
80
|
+
this.#body = content
|
|
81
|
+
this.header('Content-Type', 'text/html')
|
|
82
|
+
}
|
|
83
|
+
return this
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public send(data: any): this {
|
|
87
|
+
this.#body = data
|
|
88
|
+
return this
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public redirect(location: string, status: number = 302): this {
|
|
92
|
+
if (
|
|
93
|
+
!location.startsWith('/') &&
|
|
94
|
+
!location.startsWith('https://') &&
|
|
95
|
+
!location.startsWith('http://')
|
|
96
|
+
) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'Invalid redirect location: must be a relative path or absolute URL'
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
this.#statusCode = status
|
|
102
|
+
this.header('Location', location)
|
|
103
|
+
return this
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public close(): this {
|
|
107
|
+
if (this.#close) {
|
|
108
|
+
this.#close()
|
|
109
|
+
}
|
|
110
|
+
return this
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public toResponse(args?: Record<string, any>): Response {
|
|
114
|
+
const cookieHeader = Array.from(this.#cookies.entries()).map(
|
|
115
|
+
([name, { value, flags }]) => serializeCookie(name, value, flags)
|
|
116
|
+
)
|
|
117
|
+
// Multiple Set-Cookie headers must be appended separately, not joined with commas
|
|
118
|
+
// per HTTP specification (RFC 6265)
|
|
119
|
+
cookieHeader.forEach((cookie) => this.#headers.append('Set-Cookie', cookie))
|
|
120
|
+
return new Response(this.#body, {
|
|
121
|
+
...args,
|
|
122
|
+
status: this.#statusCode,
|
|
123
|
+
headers: this.#headers,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private createStream(): ReadableStream<Uint8Array> {
|
|
128
|
+
const encoder = new TextEncoder()
|
|
129
|
+
return new ReadableStream({
|
|
130
|
+
start: (controller) => {
|
|
131
|
+
const send = (data: string) => {
|
|
132
|
+
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const close = () => {
|
|
136
|
+
controller.close()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.#send = send
|
|
140
|
+
this.#close = close
|
|
141
|
+
|
|
142
|
+
// Force initial flush
|
|
143
|
+
controller.enqueue(encoder.encode(':\n\n'))
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HTTPMethod } from '../http.types.js'
|
|
2
|
+
import { PathToRegexRouter } from './path-to-regex.js'
|
|
3
|
+
|
|
4
|
+
export type MatchResult = {
|
|
5
|
+
route: string
|
|
6
|
+
params: Record<string, any>
|
|
7
|
+
} | null
|
|
8
|
+
|
|
9
|
+
export interface Router {
|
|
10
|
+
match(method: HTTPMethod, path: string): MatchResult
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const httpRouter = new PathToRegexRouter()
|