@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.
Files changed (229) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/dev/hot-reload.d.ts +10 -0
  3. package/dist/dev/hot-reload.js +158 -0
  4. package/dist/middleware-runner.d.ts +1 -0
  5. package/dist/middleware-runner.js +5 -0
  6. package/dist/permissions.d.ts +1 -0
  7. package/dist/permissions.js +5 -0
  8. package/dist/services/in-memory-workflow-service.js +7 -11
  9. package/dist/wirings/ai-agent/ai-agent-prepare.js +16 -1
  10. package/dist/wirings/channel/channel-middleware-runner.d.ts +1 -0
  11. package/dist/wirings/channel/channel-middleware-runner.js +5 -0
  12. package/dist/wirings/workflow/pikku-workflow-service.js +7 -8
  13. package/package.json +3 -2
  14. package/src/crypto-utils.test.ts +214 -0
  15. package/src/crypto-utils.ts +213 -0
  16. package/src/dev/hot-reload.test.ts +484 -0
  17. package/src/dev/hot-reload.ts +212 -0
  18. package/src/errors/error-handler.ts +62 -0
  19. package/src/errors/error.test.ts +195 -0
  20. package/src/errors/errors.ts +374 -0
  21. package/src/errors/index.ts +2 -0
  22. package/src/factory-functions.test.ts +109 -0
  23. package/src/function/function-runner.test.ts +536 -0
  24. package/src/function/function-runner.ts +365 -0
  25. package/src/function/functions.types.ts +304 -0
  26. package/src/function/index.ts +5 -0
  27. package/src/handle-error.test.ts +424 -0
  28. package/src/handle-error.ts +69 -0
  29. package/src/index.ts +118 -0
  30. package/src/internal.ts +6 -0
  31. package/src/middleware/auth-apikey.test.ts +363 -0
  32. package/src/middleware/auth-apikey.ts +47 -0
  33. package/src/middleware/auth-bearer.test.ts +450 -0
  34. package/src/middleware/auth-bearer.ts +72 -0
  35. package/src/middleware/auth-cookie.test.ts +528 -0
  36. package/src/middleware/auth-cookie.ts +80 -0
  37. package/src/middleware/cors.test.ts +424 -0
  38. package/src/middleware/cors.ts +104 -0
  39. package/src/middleware/index.ts +5 -0
  40. package/src/middleware/remote-auth.test.ts +488 -0
  41. package/src/middleware/remote-auth.ts +68 -0
  42. package/src/middleware/timeout.ts +15 -0
  43. package/src/middleware-runner.test.ts +418 -0
  44. package/src/middleware-runner.ts +240 -0
  45. package/src/permissions.test.ts +434 -0
  46. package/src/permissions.ts +327 -0
  47. package/src/pikku-request.ts +23 -0
  48. package/src/pikku-response.ts +5 -0
  49. package/src/pikku-state.test.ts +224 -0
  50. package/src/pikku-state.ts +216 -0
  51. package/src/run-tests-script.test.ts +49 -0
  52. package/src/schema.test.ts +249 -0
  53. package/src/schema.ts +151 -0
  54. package/src/services/ai-agent-runner-service.ts +41 -0
  55. package/src/services/ai-run-state-service.ts +20 -0
  56. package/src/services/ai-storage-service.ts +39 -0
  57. package/src/services/content-service.ts +76 -0
  58. package/src/services/deployment-service.ts +22 -0
  59. package/src/services/gateway-service.ts +20 -0
  60. package/src/services/gopass-secrets.ts +98 -0
  61. package/src/services/in-memory-ai-run-state-service.ts +73 -0
  62. package/src/services/in-memory-trigger-service.ts +64 -0
  63. package/src/services/in-memory-workflow-service.test.ts +351 -0
  64. package/src/services/in-memory-workflow-service.ts +502 -0
  65. package/src/services/index.ts +56 -0
  66. package/src/services/jwt-service.ts +30 -0
  67. package/src/services/local-content.ts +120 -0
  68. package/src/services/local-gateway-service.ts +62 -0
  69. package/src/services/local-secrets.test.ts +80 -0
  70. package/src/services/local-secrets.ts +94 -0
  71. package/src/services/local-variables.test.ts +61 -0
  72. package/src/services/local-variables.ts +39 -0
  73. package/src/services/logger-console.test.ts +118 -0
  74. package/src/services/logger-console.ts +98 -0
  75. package/src/services/logger.ts +57 -0
  76. package/src/services/scheduler-service.ts +86 -0
  77. package/src/services/schema-service.ts +33 -0
  78. package/src/services/scoped-secret-service.test.ts +74 -0
  79. package/src/services/scoped-secret-service.ts +41 -0
  80. package/src/services/secret-service.ts +38 -0
  81. package/src/services/trigger-service.ts +17 -0
  82. package/src/services/typed-secret-service.test.ts +93 -0
  83. package/src/services/typed-secret-service.ts +70 -0
  84. package/src/services/typed-variables-service.test.ts +73 -0
  85. package/src/services/typed-variables-service.ts +81 -0
  86. package/src/services/user-session-service.test.ts +113 -0
  87. package/src/services/user-session-service.ts +86 -0
  88. package/src/services/variables-service.ts +11 -0
  89. package/src/services/workflow-service.ts +95 -0
  90. package/src/testing/index.ts +2 -0
  91. package/src/testing/service-tests.ts +873 -0
  92. package/src/time-utils.test.ts +56 -0
  93. package/src/time-utils.ts +107 -0
  94. package/src/types/core.types.ts +478 -0
  95. package/src/types/state.types.ts +188 -0
  96. package/src/utils/hash.test.ts +68 -0
  97. package/src/utils/hash.ts +26 -0
  98. package/src/utils.test.ts +350 -0
  99. package/src/utils.ts +129 -0
  100. package/src/version.test.ts +80 -0
  101. package/src/version.ts +25 -0
  102. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
  103. package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
  104. package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
  105. package/src/wirings/ai-agent/ai-agent-memory.ts +310 -0
  106. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
  107. package/src/wirings/ai-agent/ai-agent-model-config.ts +43 -0
  108. package/src/wirings/ai-agent/ai-agent-prepare.ts +659 -0
  109. package/src/wirings/ai-agent/ai-agent-registry.test.ts +37 -0
  110. package/src/wirings/ai-agent/ai-agent-registry.ts +82 -0
  111. package/src/wirings/ai-agent/ai-agent-runner.test.ts +319 -0
  112. package/src/wirings/ai-agent/ai-agent-runner.ts +773 -0
  113. package/src/wirings/ai-agent/ai-agent-stream.test.ts +859 -0
  114. package/src/wirings/ai-agent/ai-agent-stream.ts +1153 -0
  115. package/src/wirings/ai-agent/ai-agent.types.ts +382 -0
  116. package/src/wirings/ai-agent/index.ts +37 -0
  117. package/src/wirings/channel/channel-common.ts +90 -0
  118. package/src/wirings/channel/channel-handler.ts +250 -0
  119. package/src/wirings/channel/channel-middleware-runner.ts +126 -0
  120. package/src/wirings/channel/channel-runner.ts +166 -0
  121. package/src/wirings/channel/channel-store.ts +29 -0
  122. package/src/wirings/channel/channel.types.ts +172 -0
  123. package/src/wirings/channel/define-channel-routes.ts +25 -0
  124. package/src/wirings/channel/eventhub-service.ts +34 -0
  125. package/src/wirings/channel/eventhub-store.ts +13 -0
  126. package/src/wirings/channel/index.ts +24 -0
  127. package/src/wirings/channel/local/index.ts +3 -0
  128. package/src/wirings/channel/local/local-channel-handler.ts +81 -0
  129. package/src/wirings/channel/local/local-channel-runner.test.ts +215 -0
  130. package/src/wirings/channel/local/local-channel-runner.ts +210 -0
  131. package/src/wirings/channel/local/local-eventhub-service.test.ts +95 -0
  132. package/src/wirings/channel/local/local-eventhub-service.ts +101 -0
  133. package/src/wirings/channel/log-channels.ts +20 -0
  134. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +54 -0
  135. package/src/wirings/channel/pikku-abstract-channel-handler.ts +45 -0
  136. package/src/wirings/channel/serverless/index.ts +5 -0
  137. package/src/wirings/channel/serverless/serverless-channel-runner.ts +289 -0
  138. package/src/wirings/cli/channel/cli-channel-runner.ts +136 -0
  139. package/src/wirings/cli/channel/index.ts +1 -0
  140. package/src/wirings/cli/cli-runner.test.ts +403 -0
  141. package/src/wirings/cli/cli-runner.ts +529 -0
  142. package/src/wirings/cli/cli.types.ts +358 -0
  143. package/src/wirings/cli/command-parser.test.ts +445 -0
  144. package/src/wirings/cli/command-parser.ts +504 -0
  145. package/src/wirings/cli/define-cli-commands.ts +24 -0
  146. package/src/wirings/cli/index.ts +19 -0
  147. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  148. package/src/wirings/gateway/gateway-runner.ts +411 -0
  149. package/src/wirings/gateway/gateway.types.ts +149 -0
  150. package/src/wirings/gateway/index.ts +13 -0
  151. package/src/wirings/http/http-routes.test.ts +322 -0
  152. package/src/wirings/http/http-routes.ts +197 -0
  153. package/src/wirings/http/http-runner.test.ts +144 -0
  154. package/src/wirings/http/http-runner.ts +588 -0
  155. package/src/wirings/http/http.types.ts +369 -0
  156. package/src/wirings/http/index.ts +28 -0
  157. package/src/wirings/http/log-http-routes.ts +22 -0
  158. package/src/wirings/http/pikku-fetch-http-request.test.ts +237 -0
  159. package/src/wirings/http/pikku-fetch-http-request.ts +170 -0
  160. package/src/wirings/http/pikku-fetch-http-response.test.ts +82 -0
  161. package/src/wirings/http/pikku-fetch-http-response.ts +147 -0
  162. package/src/wirings/http/routers/http-router.ts +13 -0
  163. package/src/wirings/http/routers/path-to-regex.test.ts +319 -0
  164. package/src/wirings/http/routers/path-to-regex.ts +126 -0
  165. package/src/wirings/http/web-request.test.ts +236 -0
  166. package/src/wirings/http/web-request.ts +104 -0
  167. package/src/wirings/mcp/index.ts +27 -0
  168. package/src/wirings/mcp/mcp-endpoint-registry.test.ts +389 -0
  169. package/src/wirings/mcp/mcp-endpoint-registry.ts +159 -0
  170. package/src/wirings/mcp/mcp-runner.ts +323 -0
  171. package/src/wirings/mcp/mcp.types.ts +216 -0
  172. package/src/wirings/node/index.ts +2 -0
  173. package/src/wirings/node/node.types.ts +23 -0
  174. package/src/wirings/oauth2/index.ts +3 -0
  175. package/src/wirings/oauth2/oauth2-client.test.ts +929 -0
  176. package/src/wirings/oauth2/oauth2-client.ts +335 -0
  177. package/src/wirings/oauth2/oauth2.types.ts +69 -0
  178. package/src/wirings/oauth2/wire-oauth2-credential.ts +23 -0
  179. package/src/wirings/queue/index.ts +31 -0
  180. package/src/wirings/queue/queue-runner.test.ts +710 -0
  181. package/src/wirings/queue/queue-runner.ts +192 -0
  182. package/src/wirings/queue/queue.types.ts +189 -0
  183. package/src/wirings/queue/register-queue-helper.ts +60 -0
  184. package/src/wirings/queue/validate-worker-config.test.ts +108 -0
  185. package/src/wirings/queue/validate-worker-config.ts +116 -0
  186. package/src/wirings/rpc/index.ts +4 -0
  187. package/src/wirings/rpc/rpc-runner.ts +460 -0
  188. package/src/wirings/rpc/rpc-types.ts +56 -0
  189. package/src/wirings/rpc/wire-addon.ts +21 -0
  190. package/src/wirings/scheduler/index.ts +11 -0
  191. package/src/wirings/scheduler/log-schedulers.ts +20 -0
  192. package/src/wirings/scheduler/scheduler-runner.test.ts +660 -0
  193. package/src/wirings/scheduler/scheduler-runner.ts +133 -0
  194. package/src/wirings/scheduler/scheduler.types.ts +53 -0
  195. package/src/wirings/secret/index.ts +9 -0
  196. package/src/wirings/secret/secret.types.ts +32 -0
  197. package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
  198. package/src/wirings/secret/validate-secret-definitions.ts +82 -0
  199. package/src/wirings/trigger/index.ts +10 -0
  200. package/src/wirings/trigger/pikku-trigger-service.ts +112 -0
  201. package/src/wirings/trigger/trigger-runner.test.ts +79 -0
  202. package/src/wirings/trigger/trigger-runner.ts +135 -0
  203. package/src/wirings/trigger/trigger.types.ts +178 -0
  204. package/src/wirings/variable/index.ts +8 -0
  205. package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
  206. package/src/wirings/variable/validate-variable-definitions.ts +69 -0
  207. package/src/wirings/variable/variable.types.ts +22 -0
  208. package/src/wirings/workflow/dsl/index.ts +31 -0
  209. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +320 -0
  210. package/src/wirings/workflow/dsl/workflow-runner.ts +28 -0
  211. package/src/wirings/workflow/graph/graph-node.ts +222 -0
  212. package/src/wirings/workflow/graph/graph-runner.test.ts +487 -0
  213. package/src/wirings/workflow/graph/graph-runner.ts +816 -0
  214. package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
  215. package/src/wirings/workflow/graph/graph-validation.ts +237 -0
  216. package/src/wirings/workflow/graph/index.ts +19 -0
  217. package/src/wirings/workflow/graph/template.test.ts +49 -0
  218. package/src/wirings/workflow/graph/template.ts +42 -0
  219. package/src/wirings/workflow/graph/wire-workflow-graph.ts +29 -0
  220. package/src/wirings/workflow/graph/workflow-graph.types.ts +104 -0
  221. package/src/wirings/workflow/index.ts +82 -0
  222. package/src/wirings/workflow/pikku-workflow-service.test.ts +200 -0
  223. package/src/wirings/workflow/pikku-workflow-service.ts +1179 -0
  224. package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
  225. package/src/wirings/workflow/workflow-helpers.ts +79 -0
  226. package/src/wirings/workflow/workflow.types.ts +274 -0
  227. package/tsconfig.json +14 -0
  228. package/tsconfig.tsbuildinfo +1 -0
  229. package/lcov.info +0 -18891
@@ -0,0 +1,212 @@
1
+ import { watch, type FSWatcher } from 'node:fs'
2
+ import { stat, readFile } from 'node:fs/promises'
3
+ import { join, resolve, relative } from 'node:path'
4
+
5
+ import { pikkuState } from '../pikku-state.js'
6
+ import { addFunction } from '../function/function-runner.js'
7
+ import { clearMiddlewareCache } from '../middleware-runner.js'
8
+ import { clearPermissionsCache } from '../permissions.js'
9
+ import { clearChannelMiddlewareCache } from '../wirings/channel/channel-middleware-runner.js'
10
+ import { httpRouter } from '../wirings/http/routers/http-router.js'
11
+ import { addSchema, compileAllSchemas } from '../schema.js'
12
+ import type { Logger } from '../services/logger.js'
13
+ import type { CorePikkuFunctionConfig } from '../function/functions.types.js'
14
+
15
+ interface PikkuDevReloaderOptions {
16
+ srcDirectories: string[]
17
+ logger: Logger
18
+ pikkuDir?: string
19
+ }
20
+
21
+ const isFunctionConfig = (
22
+ value: unknown
23
+ ): value is CorePikkuFunctionConfig<any, any> => {
24
+ return (
25
+ typeof value === 'object' &&
26
+ value !== null &&
27
+ 'func' in value &&
28
+ typeof (value as any).func === 'function'
29
+ )
30
+ }
31
+
32
+ const findCompiledFile = async (
33
+ tsFile: string,
34
+ srcDir: string,
35
+ pikkuDir: string
36
+ ): Promise<string | null> => {
37
+ const rel = relative(srcDir, tsFile).replace(/\.ts$/, '.js')
38
+ const candidates = [
39
+ join(pikkuDir, 'dist', rel),
40
+ join(srcDir, rel),
41
+ tsFile.replace(/\.ts$/, '.js'),
42
+ ]
43
+ for (const candidate of candidates) {
44
+ try {
45
+ await stat(candidate)
46
+ return candidate
47
+ } catch {
48
+ // not found, try next
49
+ }
50
+ }
51
+ return null
52
+ }
53
+
54
+ // Use data: URLs to import modules. This bypasses TypeScript loaders
55
+ // (e.g. tsx) that intercept file:// imports and break dynamic ESM loading.
56
+ // Each import gets unique content so there's no module cache to worry about.
57
+ const reimportModule = async (
58
+ filePath: string
59
+ ): Promise<Record<string, unknown> | null> => {
60
+ try {
61
+ const content = await readFile(resolve(filePath), 'utf-8')
62
+ const dataUrl =
63
+ 'data:text/javascript;base64,' + Buffer.from(content).toString('base64')
64
+ return await import(dataUrl)
65
+ } catch {
66
+ return null
67
+ }
68
+ }
69
+
70
+ const isWatchedTsFile = (filename: string): boolean => {
71
+ return (
72
+ filename.endsWith('.ts') &&
73
+ !filename.endsWith('.test.ts') &&
74
+ !filename.endsWith('.d.ts') &&
75
+ !filename.endsWith('.gen.ts')
76
+ )
77
+ }
78
+
79
+ export async function pikkuDevReloader(
80
+ options: PikkuDevReloaderOptions
81
+ ): Promise<{ close: () => void }> {
82
+ const { srcDirectories, logger, pikkuDir = '.pikku' } = options
83
+ const absSrcDirs = srcDirectories.map((d) => resolve(d))
84
+ const absPikkuDir = resolve(pikkuDir)
85
+ const watchers: FSWatcher[] = []
86
+
87
+ const functionsMap = pikkuState(null, 'function', 'functions')
88
+
89
+ const handleFileChange = async (changedTsFile: string) => {
90
+ const start = Date.now()
91
+ const reloadedNames: string[] = []
92
+
93
+ const srcDir = absSrcDirs.find((d) => changedTsFile.startsWith(d))
94
+ if (!srcDir) return
95
+
96
+ const compiledFile = await findCompiledFile(
97
+ changedTsFile,
98
+ srcDir,
99
+ absPikkuDir
100
+ )
101
+ if (!compiledFile) {
102
+ logger.warn(
103
+ `Could not find compiled JS for: ${relative(process.cwd(), changedTsFile)}`
104
+ )
105
+ return
106
+ }
107
+
108
+ const mod = await reimportModule(compiledFile)
109
+ if (!mod) {
110
+ logger.error(
111
+ `Failed to import: ${relative(process.cwd(), compiledFile)} (keeping old code)`
112
+ )
113
+ return
114
+ }
115
+
116
+ let schemasChanged = false
117
+
118
+ for (const [exportName, exportValue] of Object.entries(mod)) {
119
+ if (isFunctionConfig(exportValue) && functionsMap.has(exportName)) {
120
+ addFunction(exportName, exportValue)
121
+ reloadedNames.push(exportName)
122
+
123
+ if (exportValue.input) {
124
+ addSchema(exportName, exportValue.input)
125
+ schemasChanged = true
126
+ }
127
+ if (exportValue.output) {
128
+ addSchema(`${exportName}Output`, exportValue.output)
129
+ schemasChanged = true
130
+ }
131
+ }
132
+ }
133
+
134
+ if (reloadedNames.length > 0) {
135
+ clearMiddlewareCache()
136
+ clearPermissionsCache()
137
+ clearChannelMiddlewareCache()
138
+ httpRouter.reset()
139
+
140
+ if (schemasChanged) {
141
+ try {
142
+ compileAllSchemas(logger)
143
+ } catch (err) {
144
+ const msg = err instanceof Error ? err.message : String(err)
145
+ if (
146
+ msg.includes('SchemaService') ||
147
+ (msg.includes('schema') && msg.includes('not'))
148
+ ) {
149
+ logger.warn('Schema recompilation skipped (no SchemaService)')
150
+ } else {
151
+ logger.error(`Schema recompilation failed: ${msg}`)
152
+ return
153
+ }
154
+ }
155
+ }
156
+
157
+ const elapsed = Date.now() - start
158
+ logger.info(`Hot-reloaded: ${reloadedNames.join(', ')} (${elapsed}ms)`)
159
+ }
160
+ }
161
+
162
+ let debounceTimer: ReturnType<typeof setTimeout> | undefined
163
+ const pendingChanges = new Set<string>()
164
+
165
+ const scheduleReload = (filePath: string) => {
166
+ pendingChanges.add(filePath)
167
+ if (debounceTimer) clearTimeout(debounceTimer)
168
+ debounceTimer = setTimeout(async () => {
169
+ const files = [...pendingChanges]
170
+ pendingChanges.clear()
171
+ for (const file of files) {
172
+ try {
173
+ await handleFileChange(file)
174
+ } catch (err) {
175
+ logger.error(
176
+ `Hot-reload error for ${relative(process.cwd(), file)}: ${err instanceof Error ? err.message : String(err)}`
177
+ )
178
+ }
179
+ }
180
+ }, 50)
181
+ }
182
+
183
+ for (const srcDir of absSrcDirs) {
184
+ try {
185
+ const watcher = watch(
186
+ srcDir,
187
+ { recursive: true },
188
+ (eventType, filename) => {
189
+ if (filename && isWatchedTsFile(filename)) {
190
+ scheduleReload(join(srcDir, filename))
191
+ }
192
+ }
193
+ )
194
+ watchers.push(watcher)
195
+ } catch (err: any) {
196
+ logger.error(
197
+ `Failed to watch directory ${srcDir}: ${err?.message || err}`
198
+ )
199
+ }
200
+ }
201
+
202
+ logger.info(`Hot-reload active for: ${srcDirectories.join(', ')}`)
203
+
204
+ return {
205
+ close: () => {
206
+ if (debounceTimer) clearTimeout(debounceTimer)
207
+ for (const watcher of watchers) {
208
+ watcher.close()
209
+ }
210
+ },
211
+ }
212
+ }
@@ -0,0 +1,62 @@
1
+ import { pikkuState } from '../pikku-state.js'
2
+
3
+ /**
4
+ * Base class for custom errors.
5
+ * @extends {Error}
6
+ */
7
+ export class PikkuError extends Error {
8
+ /**
9
+ * Creates an instance of PikkuError.
10
+ * @param message - The error message.
11
+ */
12
+ constructor(message: string = 'An error occurred') {
13
+ super(message)
14
+ Object.setPrototypeOf(this, new.target.prototype)
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Interface for error details.
20
+ */
21
+ export interface ErrorDetails {
22
+ status: number
23
+ message: string
24
+ mcpCode?: number
25
+ }
26
+
27
+ /**
28
+ * Adds an error to the API errors map.
29
+ * @param error - The error to add.
30
+ * @param details - The details of the error.
31
+ */
32
+ export const addError = (error: any, { status, message }: ErrorDetails) => {
33
+ pikkuState(null, 'misc', 'errors').set(error, { status, message })
34
+ }
35
+
36
+ /**
37
+ * Adds multiple errors to the API errors map.
38
+ * @param errors - An array of errors and their details.
39
+ */
40
+ export const addErrors = (
41
+ errors: Array<[error: any, details: ErrorDetails]>
42
+ ) => {
43
+ errors.forEach((error) => {
44
+ addError(error[0], error[1])
45
+ })
46
+ }
47
+
48
+ /**
49
+ * Retrieves the error response for a given error.
50
+ * @param error - The error to get the response for.
51
+ * @returns An object containing the status and message, or undefined if the error is not found.
52
+ */
53
+ export const getErrorResponse = (
54
+ error: Error
55
+ ): { status: number; message: string; mcpCode?: number } | undefined => {
56
+ const errors = Array.from(pikkuState(null, 'misc', 'errors').entries())
57
+ const foundError = errors.find(([e]) => e.name === error.constructor.name)
58
+ if (foundError) {
59
+ return foundError[1]
60
+ }
61
+ return pikkuState(null, 'misc', 'errors').get(error)
62
+ }
@@ -0,0 +1,195 @@
1
+ import { test, describe } from 'node:test'
2
+ import assert from 'assert'
3
+ import {
4
+ getErrorResponse,
5
+ addError,
6
+ addErrors,
7
+ PikkuError,
8
+ } from './error-handler.js'
9
+ import {
10
+ BadRequestError,
11
+ UnauthorizedError,
12
+ MissingSessionError,
13
+ InvalidSessionError,
14
+ PaymentRequiredError,
15
+ ForbiddenError,
16
+ InvalidOriginError,
17
+ NotFoundError,
18
+ MethodNotAllowedError,
19
+ NotAcceptableError,
20
+ ProxyAuthenticationRequiredError,
21
+ RequestTimeoutError,
22
+ ConflictError,
23
+ GoneError,
24
+ LengthRequiredError,
25
+ PreconditionFailedError,
26
+ PayloadTooLargeError,
27
+ URITooLongError,
28
+ UnsupportedMediaTypeError,
29
+ RangeNotSatisfiableError,
30
+ ExpectationFailedError,
31
+ UnprocessableContentError,
32
+ LockedError,
33
+ TooManyRequestsError,
34
+ InternalServerError,
35
+ NotImplementedError,
36
+ BadGatewayError,
37
+ ServiceUnavailableError,
38
+ GatewayTimeoutError,
39
+ HTTPVersionNotSupportedError,
40
+ MaxComputeTimeReachedError,
41
+ MissingSchemaError,
42
+ InvalidMiddlewareWireError,
43
+ PikkuMissingMetaError,
44
+ } from './errors.js'
45
+
46
+ describe('PikkuError', () => {
47
+ test('should create error with default message', () => {
48
+ const error = new PikkuError()
49
+ assert.strictEqual(error.message, 'An error occurred')
50
+ })
51
+
52
+ test('should create error with custom message', () => {
53
+ const error = new PikkuError('custom message')
54
+ assert.strictEqual(error.message, 'custom message')
55
+ })
56
+
57
+ test('should be instance of Error', () => {
58
+ const error = new PikkuError()
59
+ assert.ok(error instanceof Error)
60
+ })
61
+
62
+ test('should be instance of PikkuError', () => {
63
+ const error = new PikkuError()
64
+ assert.ok(error instanceof PikkuError)
65
+ })
66
+
67
+ test('subclasses should be instanceof PikkuError', () => {
68
+ assert.ok(new BadRequestError() instanceof PikkuError)
69
+ assert.ok(new NotFoundError() instanceof PikkuError)
70
+ assert.ok(new ForbiddenError() instanceof PikkuError)
71
+ })
72
+ })
73
+
74
+ describe('getErrorResponse', () => {
75
+ test('should return the correct error response for BadRequestError', () => {
76
+ const error = new BadRequestError()
77
+ const response = getErrorResponse(error)
78
+ assert.deepStrictEqual(response, {
79
+ status: 400,
80
+ message:
81
+ 'The server cannot or will not process the request due to client error (e.g., malformed request syntax).',
82
+ })
83
+ })
84
+
85
+ test('should return the correct error response for NotFoundError', () => {
86
+ const error = new NotFoundError()
87
+ const response = getErrorResponse(error)
88
+ assert.deepStrictEqual(response, {
89
+ status: 404,
90
+ message: 'The server cannot find the requested resource.',
91
+ })
92
+ })
93
+
94
+ test('should return the correct error response for ForbiddenError', () => {
95
+ const error = new ForbiddenError()
96
+ const response = getErrorResponse(error)
97
+ assert.deepStrictEqual(response, {
98
+ status: 403,
99
+ message:
100
+ 'The client does not have permission to access the requested resource.',
101
+ })
102
+ })
103
+
104
+ test('should return undefined for an unknown error', () => {
105
+ class UnknownError extends Error {}
106
+ const error = new UnknownError()
107
+ const response = getErrorResponse(error)
108
+ assert.strictEqual(response, undefined)
109
+ })
110
+
111
+ test('should return the correct error response for a custom error added to apiErrors', () => {
112
+ class CustomError extends Error {}
113
+ const customError = new CustomError()
114
+ const customErrorDetails = { status: 400, message: 'Custom Error' }
115
+
116
+ // Add the custom error to the apiErrors map
117
+ addError(CustomError, customErrorDetails)
118
+
119
+ const response = getErrorResponse(customError)
120
+ assert.deepStrictEqual(response, customErrorDetails)
121
+ })
122
+ })
123
+
124
+ describe('addErrors', () => {
125
+ test('should register multiple errors at once', () => {
126
+ class ErrA extends Error {}
127
+ class ErrB extends Error {}
128
+ addErrors([
129
+ [ErrA, { status: 450, message: 'Error A' }],
130
+ [ErrB, { status: 451, message: 'Error B' }],
131
+ ])
132
+ assert.deepStrictEqual(getErrorResponse(new ErrA()), {
133
+ status: 450,
134
+ message: 'Error A',
135
+ })
136
+ assert.deepStrictEqual(getErrorResponse(new ErrB()), {
137
+ status: 451,
138
+ message: 'Error B',
139
+ })
140
+ })
141
+ })
142
+
143
+ describe('all built-in error classes', () => {
144
+ const errorCases: [string, any, number][] = [
145
+ ['InvalidMiddlewareWireError', InvalidMiddlewareWireError, 500],
146
+ ['PikkuMissingMetaError', PikkuMissingMetaError, 500],
147
+ ['BadRequestError', BadRequestError, 400],
148
+ ['UnauthorizedError', UnauthorizedError, 401],
149
+ ['MissingSessionError', MissingSessionError, 401],
150
+ ['InvalidSessionError', InvalidSessionError, 401],
151
+ ['PaymentRequiredError', PaymentRequiredError, 402],
152
+ ['ForbiddenError', ForbiddenError, 403],
153
+ ['InvalidOriginError', InvalidOriginError, 403],
154
+ ['NotFoundError', NotFoundError, 404],
155
+ ['MethodNotAllowedError', MethodNotAllowedError, 405],
156
+ ['NotAcceptableError', NotAcceptableError, 406],
157
+ ['ProxyAuthenticationRequiredError', ProxyAuthenticationRequiredError, 407],
158
+ ['RequestTimeoutError', RequestTimeoutError, 408],
159
+ ['ConflictError', ConflictError, 409],
160
+ ['GoneError', GoneError, 410],
161
+ ['LengthRequiredError', LengthRequiredError, 411],
162
+ ['PreconditionFailedError', PreconditionFailedError, 412],
163
+ ['PayloadTooLargeError', PayloadTooLargeError, 413],
164
+ ['URITooLongError', URITooLongError, 414],
165
+ ['UnsupportedMediaTypeError', UnsupportedMediaTypeError, 415],
166
+ ['RangeNotSatisfiableError', RangeNotSatisfiableError, 416],
167
+ ['ExpectationFailedError', ExpectationFailedError, 417],
168
+ ['UnprocessableContentError', UnprocessableContentError, 422],
169
+ ['LockedError', LockedError, 423],
170
+ ['TooManyRequestsError', TooManyRequestsError, 429],
171
+ ['InternalServerError', InternalServerError, 500],
172
+ ['NotImplementedError', NotImplementedError, 501],
173
+ ['BadGatewayError', BadGatewayError, 502],
174
+ ['ServiceUnavailableError', ServiceUnavailableError, 503],
175
+ ['GatewayTimeoutError', GatewayTimeoutError, 504],
176
+ ['HTTPVersionNotSupportedError', HTTPVersionNotSupportedError, 505],
177
+ ['MaxComputeTimeReachedError', MaxComputeTimeReachedError, 524],
178
+ ['MissingSchemaError', MissingSchemaError, 500],
179
+ ]
180
+
181
+ for (const [name, ErrorClass, expectedStatus] of errorCases) {
182
+ test(`${name} should map to status ${expectedStatus}`, () => {
183
+ const error = new ErrorClass()
184
+ const response = getErrorResponse(error)
185
+ assert.ok(response, `Expected error response for ${name}`)
186
+ assert.strictEqual(response!.status, expectedStatus)
187
+ assert.ok(response!.message.length > 0)
188
+ })
189
+
190
+ test(`${name} should be instance of PikkuError`, () => {
191
+ const error = new ErrorClass()
192
+ assert.ok(error instanceof PikkuError)
193
+ })
194
+ }
195
+ })