@pikku/core 0.6.26 → 0.7.0

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 (130) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/dist/channel/channel-handler.js +16 -38
  3. package/dist/channel/channel-runner.d.ts +7 -8
  4. package/dist/channel/channel-runner.js +58 -7
  5. package/dist/channel/channel.types.d.ts +14 -27
  6. package/dist/channel/local/local-channel-runner.js +9 -4
  7. package/dist/channel/log-channels.js +4 -4
  8. package/dist/channel/serverless/serverless-channel-runner.js +17 -6
  9. package/dist/function/function-runner.d.ts +12 -0
  10. package/dist/function/function-runner.js +38 -0
  11. package/dist/function/functions.types.d.ts +37 -0
  12. package/dist/function/functions.types.js +1 -0
  13. package/dist/function/index.d.ts +2 -0
  14. package/dist/function/index.js +2 -0
  15. package/dist/handle-error.d.ts +1 -1
  16. package/dist/http/http-route-runner.d.ts +3 -3
  17. package/dist/http/http-route-runner.js +60 -35
  18. package/dist/http/http-routes.types.d.ts +38 -7
  19. package/dist/http/http-runner.d.ts +90 -0
  20. package/dist/http/http-runner.js +336 -0
  21. package/dist/http/http.types.d.ts +177 -0
  22. package/dist/http/http.types.js +1 -0
  23. package/dist/http/index.d.ts +2 -2
  24. package/dist/http/index.js +1 -1
  25. package/dist/http/log-http-routes.js +7 -5
  26. package/dist/http/pikku-fetch-http-request.d.ts +1 -1
  27. package/dist/http/pikku-fetch-http-response.d.ts +4 -2
  28. package/dist/http/pikku-fetch-http-response.js +64 -12
  29. package/dist/http/pikku-http-request.d.ts +56 -0
  30. package/dist/http/pikku-http-request.js +95 -0
  31. package/dist/http/pikku-http-response.d.ts +13 -0
  32. package/dist/http/pikku-http-response.js +60 -0
  33. package/dist/index.d.ts +3 -2
  34. package/dist/index.js +3 -2
  35. package/dist/middleware/auth-apikey.js +5 -4
  36. package/dist/middleware/auth-bearer.js +5 -4
  37. package/dist/middleware/auth-cookie.js +10 -9
  38. package/dist/parse-relative-time-offset.d.ts +12 -0
  39. package/dist/parse-relative-time-offset.js +20 -0
  40. package/dist/permissions.d.ts +2 -2
  41. package/dist/pikku-func.d.ts +12 -0
  42. package/dist/pikku-func.js +39 -0
  43. package/dist/pikku-function.d.ts +1 -0
  44. package/dist/pikku-function.js +1 -0
  45. package/dist/pikku-state.d.ts +10 -5
  46. package/dist/pikku-state.js +7 -3
  47. package/dist/scheduler/scheduler-runner.d.ts +1 -1
  48. package/dist/scheduler/scheduler-runner.js +24 -6
  49. package/dist/scheduler/scheduler.types.d.ts +3 -2
  50. package/dist/services/content-service.d.ts +34 -36
  51. package/dist/services/index.d.ts +1 -0
  52. package/dist/services/index.js +1 -0
  53. package/dist/services/local-content.d.ts +4 -2
  54. package/dist/services/local-content.js +19 -9
  55. package/dist/types/core.types.d.ts +15 -1
  56. package/dist/types/functions.types.d.ts +11 -6
  57. package/dist/utils.d.ts +2 -0
  58. package/dist/utils.js +15 -0
  59. package/package.json +2 -1
  60. package/lcov.info +0 -4310
  61. package/src/channel/channel-handler.ts +0 -227
  62. package/src/channel/channel-runner.ts +0 -98
  63. package/src/channel/channel-store.ts +0 -29
  64. package/src/channel/channel.types.ts +0 -171
  65. package/src/channel/eventhub-service.ts +0 -30
  66. package/src/channel/eventhub-store.ts +0 -8
  67. package/src/channel/index.ts +0 -8
  68. package/src/channel/local/index.ts +0 -3
  69. package/src/channel/local/local-channel-handler.ts +0 -50
  70. package/src/channel/local/local-channel-runner.test.ts +0 -158
  71. package/src/channel/local/local-channel-runner.ts +0 -126
  72. package/src/channel/local/local-eventhub-service.test.ts +0 -95
  73. package/src/channel/local/local-eventhub-service.ts +0 -95
  74. package/src/channel/log-channels.ts +0 -20
  75. package/src/channel/pikku-abstract-channel-handler.test.ts +0 -52
  76. package/src/channel/pikku-abstract-channel-handler.ts +0 -38
  77. package/src/channel/serverless/index.ts +0 -1
  78. package/src/channel/serverless/serverless-channel-runner.ts +0 -209
  79. package/src/errors/error-handler.ts +0 -61
  80. package/src/errors/error.test.ts +0 -54
  81. package/src/errors/errors.ts +0 -339
  82. package/src/errors/index.ts +0 -2
  83. package/src/handle-error.ts +0 -73
  84. package/src/http/http-route-runner.test.ts +0 -117
  85. package/src/http/http-route-runner.ts +0 -460
  86. package/src/http/http-routes.types.ts +0 -211
  87. package/src/http/incomingmessage-to-request-convertor.ts +0 -0
  88. package/src/http/index.ts +0 -8
  89. package/src/http/log-http-routes.ts +0 -20
  90. package/src/http/pikku-fetch-http-request.test.ts +0 -237
  91. package/src/http/pikku-fetch-http-request.ts +0 -167
  92. package/src/http/pikku-fetch-http-response.test.ts +0 -82
  93. package/src/http/pikku-fetch-http-response.ts +0 -84
  94. package/src/index.ts +0 -19
  95. package/src/middleware/auth-apikey.ts +0 -65
  96. package/src/middleware/auth-bearer.ts +0 -65
  97. package/src/middleware/auth-cookie.ts +0 -98
  98. package/src/middleware/index.ts +0 -3
  99. package/src/middleware/timeout.ts +0 -13
  100. package/src/middleware-runner.ts +0 -43
  101. package/src/permissions.test.ts +0 -58
  102. package/src/permissions.ts +0 -45
  103. package/src/pikku-request.ts +0 -23
  104. package/src/pikku-response.ts +0 -5
  105. package/src/pikku-state.ts +0 -71
  106. package/src/scheduler/index.ts +0 -5
  107. package/src/scheduler/log-schedulers.ts +0 -20
  108. package/src/scheduler/scheduler-runner.ts +0 -87
  109. package/src/scheduler/scheduler.types.ts +0 -29
  110. package/src/schema.test.ts +0 -57
  111. package/src/schema.ts +0 -99
  112. package/src/services/content-service.ts +0 -69
  113. package/src/services/index.ts +0 -17
  114. package/src/services/jwt-service.ts +0 -30
  115. package/src/services/local-content.ts +0 -91
  116. package/src/services/local-secrets.ts +0 -43
  117. package/src/services/local-variables.ts +0 -17
  118. package/src/services/logger-console.ts +0 -97
  119. package/src/services/logger.ts +0 -57
  120. package/src/services/schema-service.ts +0 -26
  121. package/src/services/secret-service.ts +0 -17
  122. package/src/services/user-session-service.ts +0 -53
  123. package/src/services/variables-service.ts +0 -6
  124. package/src/time-utils.test.ts +0 -56
  125. package/src/time-utils.ts +0 -32
  126. package/src/types/core.types.ts +0 -159
  127. package/src/types/functions.types.ts +0 -63
  128. package/src/utils.ts +0 -21
  129. package/tsconfig.json +0 -13
  130. package/tsconfig.tsbuildinfo +0 -1
@@ -1,211 +0,0 @@
1
- import { SerializeOptions } from 'cookie'
2
- import { PikkuError } from '../errors/error-handler.js'
3
- import {
4
- APIDocs,
5
- CoreServices,
6
- CoreSingletonServices,
7
- CoreUserSession,
8
- CreateSessionServices,
9
- PikkuMiddleware,
10
- } from '../types/core.types.js'
11
- import {
12
- CoreAPIFunction,
13
- CoreAPIFunctionSessionless,
14
- CoreAPIPermission,
15
- } from '../types/functions.types.js'
16
-
17
- type ExtractRouteParams<S extends string> =
18
- S extends `${string}:${infer Param}/${infer Rest}`
19
- ? Param | ExtractRouteParams<`/${Rest}`>
20
- : S extends `${string}:${infer Param}`
21
- ? Param
22
- : never
23
-
24
- export type AssertRouteParams<In, Route extends string> =
25
- ExtractRouteParams<Route> extends keyof In
26
- ? unknown
27
- : ['Error: Route parameters', ExtractRouteParams<Route>, 'not in', keyof In]
28
-
29
- export type RunRouteOptions = Partial<{
30
- skipUserSession: boolean
31
- respondWith404: boolean
32
- logWarningsForStatusCodes: number[]
33
- coerceDataFromSchema: boolean
34
- bubbleErrors: boolean
35
- }>
36
-
37
- export type RunRouteParams = {
38
- singletonServices: CoreSingletonServices
39
- createSessionServices: CreateSessionServices<
40
- CoreSingletonServices,
41
- CoreServices<CoreSingletonServices>,
42
- CoreUserSession
43
- >
44
- }
45
-
46
- /**
47
- * Represents the HTTP methods supported for API routes.
48
- */
49
- export type HTTPMethod =
50
- | 'post'
51
- | 'get'
52
- | 'delete'
53
- | 'patch'
54
- | 'head'
55
- | 'put'
56
- | 'options'
57
-
58
- /**
59
- * Represents an API route without a function, including metadata such as content type, route, and timeout settings.
60
- */
61
- export type CoreHTTPFunction = {
62
- contentType?: 'xml' | 'json'
63
- route: string
64
- eventChannel?: false
65
- returnsJSON?: false
66
- timeout?: number
67
- docs?: Partial<{
68
- description: string
69
- response: string
70
- errors: Array<typeof PikkuError>
71
- tags: string[]
72
- }>
73
- }
74
- /**
75
- * Represents a http interaction within Pikku, including a request and response.
76
- */
77
- export interface PikkuHTTP {
78
- request?: PikkuHTTPRequest
79
- response?: PikkuHTTPResponse
80
- }
81
-
82
- /**
83
- * Represents request headers as either a record or a function to get headers by name.
84
- */
85
- export type RequestHeaders =
86
- | Record<string, string | string[] | undefined>
87
- | ((headerName: string) => string | string[] | undefined)
88
-
89
- /**
90
- * Represents a query object for Pikku, where each key can be a string, a value, or an array of values.
91
- */
92
- export type PikkuQuery<T = Record<string, string | undefined>> = Record<
93
- string,
94
- string | T | null | Array<T | null>
95
- >
96
-
97
- /**
98
- * Represents a core API route, which can have different configurations depending on whether it requires authentication and permissions.
99
- *
100
- * @template In - The input type.
101
- * @template Out - The output type.
102
- * @template R - The route string type.
103
- * @template APIFunction - The API function type, defaults to `CoreAPIFunction`.
104
- * @template APIFunctionSessionless - The sessionless API function type, defaults to `CoreAPIFunctionSessionless`.
105
- * @template APIPermission - The permission function type, defaults to `CoreAPIPermission`.
106
- */
107
- export type CoreHTTPFunctionRoute<
108
- In,
109
- Out,
110
- R extends string,
111
- APIFunction = CoreAPIFunction<In, Out>,
112
- APIFunctionSessionless = CoreAPIFunctionSessionless<In, Out>,
113
- APIPermission = CoreAPIPermission<In>,
114
- APIMiddleware = PikkuMiddleware,
115
- > =
116
- | (CoreHTTPFunction & {
117
- route: R
118
- method: HTTPMethod
119
- func: APIFunction
120
- permissions?: Record<string, APIPermission[] | APIPermission>
121
- auth?: true
122
- tags?: string[]
123
- middleware?: APIMiddleware[]
124
- })
125
- | (CoreHTTPFunction & {
126
- route: R
127
- method: HTTPMethod
128
- func: APIFunctionSessionless
129
- permissions?: undefined
130
- auth?: false
131
- tags?: string[]
132
- middleware?: APIMiddleware[]
133
- })
134
- | (CoreHTTPFunction & {
135
- route: R
136
- method: 'post'
137
- func: APIFunction
138
- permissions?: Record<string, APIPermission[] | APIPermission>
139
- auth?: true
140
- query?: Array<keyof In>
141
- tags?: string[]
142
- middleware?: APIMiddleware[]
143
- })
144
- | (CoreHTTPFunction & {
145
- route: R
146
- method: 'post'
147
- func: APIFunctionSessionless
148
- permissions?: undefined
149
- auth?: false
150
- query?: Array<keyof In>
151
- tags?: string[]
152
- middleware?: APIMiddleware[]
153
- })
154
-
155
- /**
156
- * Represents an array of core API routes.
157
- */
158
- export type CoreHTTPFunctionRoutes = Array<
159
- CoreHTTPFunctionRoute<any, any, string>
160
- >
161
-
162
- /**
163
- * Represents the input types for route metadata, including parameters, query, and body types.
164
- */
165
- export type HTTPFunctionMetaInputTypes = {
166
- params?: string
167
- query?: string
168
- body?: string
169
- }
170
-
171
- /**
172
- * Represents metadata for a set of routes, including route details, methods, input/output types, and documentation.
173
- */
174
- export type HTTPRoutesMeta = Array<{
175
- route: string
176
- method: HTTPMethod
177
- params?: string[]
178
- query?: string[]
179
- input: string | null
180
- output: string | null
181
- inputTypes?: HTTPFunctionMetaInputTypes
182
- docs?: APIDocs
183
- tags?: string[]
184
- }>
185
-
186
- export type HTTPRouteMiddleware = {
187
- route: string
188
- middleware: PikkuMiddleware[]
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
- }
File without changes
package/src/http/index.ts DELETED
@@ -1,8 +0,0 @@
1
- export * from './pikku-fetch-http-request.js'
2
- export * from './pikku-fetch-http-response.js'
3
- export * from './incomingmessage-to-request-convertor.js'
4
- export * from './log-http-routes.js'
5
-
6
- export { fetch, fetchData, addRoute } from './http-route-runner.js'
7
-
8
- export type * from './http-routes.types.js'
@@ -1,20 +0,0 @@
1
- import { pikkuState } from '../pikku-state.js'
2
- import { Logger } from '../services/index.js'
3
-
4
- /**
5
- * Logs all the loaded routes.
6
- * @param logger - A logger for logging information.
7
- */
8
- export const logRoutes = (logger: Logger) => {
9
- const routes = pikkuState('http', 'routes')
10
- if (routes.length === 0) {
11
- logger.info('No routes added')
12
- return
13
- }
14
-
15
- let routesMessage = 'Routes loaded:'
16
- for (const { method, route } of routes) {
17
- routesMessage += `\n\t- ${method.toUpperCase()} -> ${route}`
18
- }
19
- logger.info(routesMessage)
20
- }
@@ -1,237 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { PikkuFetchHTTPRequest } from './pikku-fetch-http-request'
4
-
5
- const createRequest = (method, url, body, headers = {}) => {
6
- return new Request(url, {
7
- method,
8
- headers,
9
- body:
10
- method === 'POST'
11
- ? typeof body === 'string'
12
- ? body
13
- : JSON.stringify(body)
14
- : undefined,
15
- })
16
- }
17
-
18
- test('method() returns lowercase HTTP method', () => {
19
- const req = createRequest('POST', 'http://localhost/test', null)
20
- const pikkuReq = new PikkuFetchHTTPRequest(req)
21
- assert.equal(pikkuReq.method(), 'post')
22
- })
23
-
24
- test('path() returns pathname only', () => {
25
- const req = createRequest('GET', 'http://localhost/foo/bar?x=1', null)
26
- const pikkuReq = new PikkuFetchHTTPRequest(req)
27
- assert.equal(pikkuReq.path(), '/foo/bar')
28
- })
29
-
30
- test('header() retrieves headers case-insensitively', () => {
31
- const req = createRequest('GET', 'http://localhost', null, {
32
- 'Content-Type': 'application/json',
33
- })
34
- const pikkuReq = new PikkuFetchHTTPRequest(req)
35
- assert.equal(pikkuReq.header('content-type'), 'application/json')
36
- })
37
-
38
- test('cookie() parses cookies correctly', () => {
39
- const req = createRequest('GET', 'http://localhost', null, {
40
- Cookie: 'session=abc; user=test',
41
- })
42
- const pikkuReq = new PikkuFetchHTTPRequest(req)
43
- assert.equal(pikkuReq.cookie('session'), 'abc')
44
- assert.equal(pikkuReq.cookie('user'), 'test')
45
- assert.equal(pikkuReq.cookie('missing'), null)
46
- })
47
-
48
- test('params() and setParams()', () => {
49
- const req = createRequest('GET', 'http://localhost', null)
50
- const pikkuReq = new PikkuFetchHTTPRequest(req)
51
- assert.deepEqual(pikkuReq.params(), {})
52
- pikkuReq.setParams({ id: '123' })
53
- assert.deepEqual(pikkuReq.params(), { id: '123' })
54
- })
55
-
56
- test('query() parses URL search params', () => {
57
- const req = createRequest('GET', 'http://localhost?x=1&y=2', null)
58
- const pikkuReq = new PikkuFetchHTTPRequest(req)
59
- assert.equal(pikkuReq.query().x, '1')
60
- assert.equal(pikkuReq.query().y, '2')
61
- })
62
-
63
- test('data() merges json body, query, and params', async () => {
64
- const req = createRequest(
65
- 'POST',
66
- 'http://localhost/test?a=5',
67
- { foo: 'bar' },
68
- { 'Content-Type': 'application/json' }
69
- )
70
- const pikkuReq = new PikkuFetchHTTPRequest(req)
71
- pikkuReq.setParams({ id: '22' })
72
- const result = await pikkuReq.data()
73
- assert.deepEqual(result, { id: '22', a: '5', foo: 'bar' })
74
- })
75
-
76
- test('data() wraps string JSON body under data key', async () => {
77
- const req = createRequest('POST', 'http://localhost', '"hello"', {
78
- 'Content-Type': 'application/json',
79
- })
80
- const pikkuReq = new PikkuFetchHTTPRequest(req)
81
- const result = await pikkuReq.data()
82
- assert.deepEqual(result, { data: 'hello' })
83
- })
84
-
85
- test('data() handles text/plain correctly', async () => {
86
- const req = new Request('http://localhost', {
87
- method: 'POST',
88
- headers: { 'Content-Type': 'text/plain' },
89
- body: 'hello world',
90
- })
91
- const pikkuReq = new PikkuFetchHTTPRequest(req)
92
- const result = await pikkuReq.data()
93
- assert.deepEqual(result, { data: 'hello world' })
94
- })
95
-
96
- test('data() returns arrayBuffer for unknown content-type', async () => {
97
- const buffer = Buffer.from('raw')
98
- const req = new Request('http://localhost', {
99
- method: 'POST',
100
- headers: { 'Content-Type': 'application/octet-stream' },
101
- body: buffer,
102
- })
103
- const pikkuReq = new PikkuFetchHTTPRequest<any>(req)
104
- const result = await pikkuReq.data()
105
- assert(result.data instanceof ArrayBuffer)
106
- const str = Buffer.from(result.data).toString()
107
- assert.equal(str, 'raw')
108
- })
109
-
110
- test('data() handles invalid JSON safely', async () => {
111
- const req = new Request('http://localhost', {
112
- method: 'POST',
113
- headers: { 'Content-Type': 'application/json' },
114
- body: 'not-json',
115
- })
116
- const pikkuReq = new PikkuFetchHTTPRequest(req)
117
- try {
118
- await pikkuReq.data()
119
- assert(false, 'Should have thrown')
120
- } catch (e) {
121
- assert(e.message.includes('Error parsing body'))
122
- }
123
- })
124
-
125
- test('data() parses application/x-www-form-urlencoded correctly', async () => {
126
- const formBody = 'name=Yasser&age=35&active=true'
127
-
128
- const req = new Request('http://localhost/form?ref=abc', {
129
- method: 'POST',
130
- headers: {
131
- 'Content-Type': 'application/x-www-form-urlencoded',
132
- },
133
- body: formBody,
134
- })
135
-
136
- const pikkuReq = new PikkuFetchHTTPRequest(req)
137
- pikkuReq.setParams({ userId: '999' })
138
-
139
- const result = await pikkuReq.data()
140
-
141
- assert.deepEqual(result, {
142
- userId: '999',
143
- ref: 'abc',
144
- name: 'Yasser',
145
- age: '35',
146
- active: 'true',
147
- })
148
- })
149
-
150
- // --- Compatible types
151
-
152
- test('data() treats "123" and 123 as equivalent', async () => {
153
- const req = createRequest(
154
- 'POST',
155
- 'http://localhost?id=123',
156
- { id: 123 },
157
- {
158
- 'Content-Type': 'application/json',
159
- }
160
- )
161
-
162
- const r = new PikkuFetchHTTPRequest(req)
163
- r.setParams({ id: '123' }) // All match
164
- const result = await r.data()
165
- assert.deepEqual(result, { id: '123' })
166
- })
167
-
168
- test('data() treats "true" and true as equivalent', async () => {
169
- const req = createRequest(
170
- 'POST',
171
- 'http://localhost?flag=true',
172
- { flag: true },
173
- {
174
- 'Content-Type': 'application/json',
175
- }
176
- )
177
-
178
- const r = new PikkuFetchHTTPRequest(req)
179
- r.setParams({ flag: 'true' }) // All match
180
- const result = await r.data()
181
- assert.deepEqual(result, { flag: 'true' })
182
- })
183
-
184
- // --- Conflicts
185
-
186
- test('data() throws on conflicting values', async () => {
187
- const req = createRequest(
188
- 'POST',
189
- 'http://localhost?foo=123',
190
- { foo: 456 },
191
- {
192
- 'Content-Type': 'application/json',
193
- }
194
- )
195
-
196
- const r = new PikkuFetchHTTPRequest(req)
197
- r.setParams({ foo: '123' })
198
-
199
- await assert.rejects(async () => await r.data(), {
200
- message: 'Conflicting values for key "foo": "123" vs "456"',
201
- })
202
- })
203
-
204
- test('data() throws on boolean conflict', async () => {
205
- const req = createRequest(
206
- 'POST',
207
- 'http://localhost?debug=false',
208
- { debug: true },
209
- {
210
- 'Content-Type': 'application/json',
211
- }
212
- )
213
-
214
- const r = new PikkuFetchHTTPRequest(req)
215
- r.setParams({})
216
-
217
- await assert.rejects(async () => await r.data(), {
218
- message: 'Conflicting values for key "debug": "false" vs "true"',
219
- })
220
- })
221
-
222
- // --- Safe fallback: only one source
223
-
224
- test('data() works when only body has values', async () => {
225
- const req = createRequest(
226
- 'POST',
227
- 'http://localhost',
228
- { test: 'ok' },
229
- {
230
- 'Content-Type': 'application/json',
231
- }
232
- )
233
-
234
- const r = new PikkuFetchHTTPRequest(req)
235
- const result = await r.data()
236
- assert.deepEqual(result, { test: 'ok' })
237
- })
@@ -1,167 +0,0 @@
1
- import { parse as parseQuery } from 'picoquery'
2
- import { parse as parseCookie } from 'cookie'
3
- import {
4
- HTTPMethod,
5
- PikkuHTTPRequest,
6
- PikkuQuery,
7
- } from './http-routes.types.js'
8
- import { UnprocessableContentError } from '../errors/errors.js'
9
-
10
- /**
11
- * Abstract class representing a pikku request.
12
- * @template In - The type of the request body.
13
- * @group RequestResponse
14
- */
15
- export class PikkuFetchHTTPRequest<In = unknown>
16
- implements PikkuHTTPRequest<In>
17
- {
18
- #cookies: Partial<Record<string, string>> | undefined
19
- #params: Partial<Record<string, string | string[]>> = {}
20
- #url: URL
21
-
22
- constructor(private request: Request) {
23
- this.#url = new URL(request.url)
24
- }
25
-
26
- public method(): HTTPMethod {
27
- return this.request.method.toLowerCase() as HTTPMethod
28
- }
29
-
30
- public path(): string {
31
- return this.#url.pathname
32
- }
33
-
34
- /**
35
- * Retrieves the request body.
36
- * @returns A promise that resolves to the request body.
37
- */
38
- public json(): Promise<In> {
39
- return this.request.json()
40
- }
41
-
42
- /**
43
- * Retrieves the raw request body as a Buffer.
44
- * @returns A promise that resolves to the raw request body.
45
- */
46
- public arrayBuffer(): Promise<ArrayBuffer> {
47
- return this.request.arrayBuffer()
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
- const cookieHeader = this.header('cookie')
65
- this.#cookies = cookieHeader ? parseCookie(cookieHeader) : {}
66
- return this.#cookies[cookieName] || null
67
- }
68
-
69
- /**
70
- * Retrieves the request parameters.
71
- * @returns An object containing the request parameters.
72
- */
73
- public params(): Partial<Record<string, string | string[]>> {
74
- return this.#params
75
- }
76
-
77
- /**
78
- * Sets the request parameters.
79
- * @param params - An object containing the request parameters to set.
80
- */
81
- public setParams(
82
- params: Record<string, string | string[] | undefined>
83
- ): void {
84
- this.#params = params
85
- }
86
-
87
- /**
88
- * Retrieves the query parameters from the request.
89
- * @returns An object containing the query parameters.
90
- */
91
- public query(): PikkuQuery {
92
- return parseQuery(this.#url.searchParams.toString()) as PikkuQuery
93
- }
94
-
95
- /**
96
- * Retrieves the combined data from the request, including parameters, query, and body.
97
- * @returns A promise that resolves to an object containing the combined data.
98
- */
99
- public async data(): Promise<In> {
100
- const body = await this.body()
101
- const parts = [this.params(), this.query(), body]
102
- const merged: Record<string, unknown> = {}
103
- for (const part of parts) {
104
- for (const [key, value] of Object.entries(part)) {
105
- if (key in merged && !valuesAreEquivalent(merged[key], value)) {
106
- throw new UnprocessableContentError(
107
- `Conflicting values for key "${key}": "${merged[key]}" vs "${value}"`
108
- )
109
- }
110
- merged[key] ??= value
111
- }
112
- }
113
- return merged as In
114
- }
115
-
116
- private async body(): Promise<any> {
117
- const noBodyMethods: HTTPMethod[] = ['get', 'head', 'options']
118
- if (noBodyMethods.includes(this.method())) {
119
- return {}
120
- }
121
-
122
- let body: any = {}
123
- const contentType = this.header('content-type') || ''
124
- try {
125
- if (contentType.includes('application/json')) {
126
- const parsed = await this.json()
127
- body =
128
- typeof parsed === 'object' &&
129
- parsed !== null &&
130
- !Array.isArray(parsed)
131
- ? parsed
132
- : { data: parsed }
133
- } else if (contentType.includes('text/')) {
134
- const text = await this.request.text()
135
- body = { data: text }
136
- } else if (contentType.includes('application/octet-stream')) {
137
- const buffer = await this.request.arrayBuffer()
138
- body = { data: buffer }
139
- } else if (contentType === 'application/x-www-form-urlencoded') {
140
- const text = await this.request.text()
141
- body = Object.fromEntries(new URLSearchParams(text))
142
- } else {
143
- throw new UnprocessableContentError(
144
- `Unsupported content type ${contentType}`
145
- )
146
- }
147
- } catch (e) {
148
- throw new UnprocessableContentError(`Error parsing body: ${e}`)
149
- }
150
- return body
151
- }
152
- }
153
-
154
- function valuesAreEquivalent(a: unknown, b: unknown): boolean {
155
- return coerce(a) === coerce(b)
156
- }
157
-
158
- function coerce(value: unknown): string | number | boolean {
159
- if (typeof value === 'boolean' || typeof value === 'number') return value
160
- if (typeof value === 'string') {
161
- if (value === 'true') return true
162
- if (value === 'false') return false
163
- const num = Number(value)
164
- return isNaN(num) ? value : num
165
- }
166
- return value as any
167
- }