@pikku/inspector 0.11.2 → 0.12.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 (182) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/OPTIMIZATION-PLAN.md +195 -0
  3. package/dist/add/add-ai-agent.d.ts +2 -0
  4. package/dist/add/add-ai-agent.js +314 -0
  5. package/dist/add/add-channel.js +69 -61
  6. package/dist/add/add-cli.js +36 -18
  7. package/dist/add/add-file-with-factory.js +2 -0
  8. package/dist/add/add-functions.js +250 -75
  9. package/dist/add/add-http-route.d.ts +19 -10
  10. package/dist/add/add-http-route.js +152 -66
  11. package/dist/add/add-http-routes.d.ts +5 -0
  12. package/dist/add/add-http-routes.js +159 -0
  13. package/dist/add/add-keyed-wiring.d.ts +12 -0
  14. package/dist/add/add-keyed-wiring.js +97 -0
  15. package/dist/add/add-mcp-prompt.js +14 -9
  16. package/dist/add/add-mcp-resource.js +14 -9
  17. package/dist/add/add-middleware.d.ts +1 -4
  18. package/dist/add/add-middleware.js +364 -79
  19. package/dist/add/add-permission.d.ts +1 -1
  20. package/dist/add/add-permission.js +152 -40
  21. package/dist/add/add-queue-worker.js +18 -12
  22. package/dist/add/add-rpc-invocations.js +14 -0
  23. package/dist/add/add-schedule.js +11 -5
  24. package/dist/add/add-secret.d.ts +3 -0
  25. package/dist/add/add-secret.js +82 -0
  26. package/dist/add/add-trigger.d.ts +2 -0
  27. package/dist/add/add-trigger.js +87 -0
  28. package/dist/add/add-variable.d.ts +1 -0
  29. package/dist/add/add-variable.js +8 -0
  30. package/dist/add/add-workflow-graph.d.ts +3 -2
  31. package/dist/add/add-workflow-graph.js +143 -406
  32. package/dist/add/add-workflow.js +6 -4
  33. package/dist/error-codes.d.ts +14 -1
  34. package/dist/error-codes.js +19 -1
  35. package/dist/index.d.ts +9 -8
  36. package/dist/index.js +5 -4
  37. package/dist/inspector.d.ts +1 -1
  38. package/dist/inspector.js +91 -14
  39. package/dist/schema-generator.d.ts +1 -0
  40. package/dist/schema-generator.js +1 -0
  41. package/dist/types-map.js +10 -1
  42. package/dist/types.d.ts +163 -39
  43. package/dist/utils/compute-required-schemas.d.ts +4 -0
  44. package/dist/utils/compute-required-schemas.js +41 -0
  45. package/dist/utils/contract-hashes.d.ts +35 -0
  46. package/dist/utils/contract-hashes.js +202 -0
  47. package/dist/utils/custom-types-generator.d.ts +9 -0
  48. package/dist/utils/custom-types-generator.js +71 -0
  49. package/dist/utils/detect-schema-vendor.d.ts +22 -0
  50. package/dist/utils/detect-schema-vendor.js +76 -0
  51. package/dist/utils/ensure-function-metadata.d.ts +5 -2
  52. package/dist/utils/ensure-function-metadata.js +220 -6
  53. package/dist/utils/extract-function-name.d.ts +5 -16
  54. package/dist/utils/extract-function-name.js +86 -291
  55. package/dist/utils/extract-services.d.ts +2 -1
  56. package/dist/utils/extract-services.js +25 -1
  57. package/dist/utils/filter-inspector-state.js +107 -23
  58. package/dist/utils/get-property-value.d.ts +6 -1
  59. package/dist/utils/get-property-value.js +28 -3
  60. package/dist/utils/hash.d.ts +2 -0
  61. package/dist/utils/hash.js +23 -0
  62. package/dist/utils/middleware.d.ts +7 -30
  63. package/dist/utils/middleware.js +80 -66
  64. package/dist/utils/permissions.d.ts +2 -2
  65. package/dist/utils/permissions.js +10 -10
  66. package/dist/utils/post-process.d.ts +9 -10
  67. package/dist/utils/post-process.js +231 -24
  68. package/dist/utils/resolve-external-package.d.ts +12 -0
  69. package/dist/utils/resolve-external-package.js +34 -0
  70. package/dist/utils/resolve-function-types.d.ts +6 -0
  71. package/dist/utils/resolve-function-types.js +29 -0
  72. package/dist/utils/resolve-identifier.d.ts +10 -0
  73. package/dist/utils/resolve-identifier.js +36 -0
  74. package/dist/utils/resolve-versions.d.ts +2 -0
  75. package/dist/utils/resolve-versions.js +78 -0
  76. package/dist/utils/schema-generator.d.ts +9 -0
  77. package/dist/utils/schema-generator.js +209 -0
  78. package/dist/utils/serialize-inspector-state.d.ts +59 -22
  79. package/dist/utils/serialize-inspector-state.js +92 -20
  80. package/dist/utils/serialize-mcp-json.d.ts +2 -0
  81. package/dist/utils/serialize-mcp-json.js +99 -0
  82. package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
  83. package/dist/utils/serialize-middleware-groups-meta.js +28 -0
  84. package/dist/utils/serialize-openapi-json.d.ts +85 -0
  85. package/dist/utils/serialize-openapi-json.js +151 -0
  86. package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
  87. package/dist/utils/serialize-permissions-groups-meta.js +31 -0
  88. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +34 -102
  89. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +23 -4
  90. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +12 -10
  91. package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
  92. package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
  93. package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
  94. package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
  95. package/dist/utils/workflow/graph/index.d.ts +2 -0
  96. package/dist/utils/workflow/graph/index.js +2 -0
  97. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +0 -8
  98. package/dist/utils/workflow/graph/serialize-workflow-graph.js +1 -3
  99. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +53 -79
  100. package/dist/utils/workflow/graph/workflow-graph.types.js +1 -1
  101. package/dist/visit.js +11 -6
  102. package/package.json +14 -4
  103. package/src/add/add-ai-agent.ts +468 -0
  104. package/src/add/add-channel.ts +82 -79
  105. package/src/add/add-cli.ts +49 -20
  106. package/src/add/add-file-with-factory.ts +2 -0
  107. package/src/add/add-functions.ts +330 -86
  108. package/src/add/add-http-route.ts +245 -88
  109. package/src/add/add-http-routes.ts +228 -0
  110. package/src/add/add-keyed-wiring.ts +151 -0
  111. package/src/add/add-mcp-prompt.ts +26 -15
  112. package/src/add/add-mcp-resource.ts +27 -15
  113. package/src/add/add-middleware.ts +482 -80
  114. package/src/add/add-permission.ts +199 -40
  115. package/src/add/add-queue-worker.ts +24 -19
  116. package/src/add/add-rpc-invocations.ts +17 -0
  117. package/src/add/add-schedule.ts +16 -11
  118. package/src/add/add-secret.ts +140 -0
  119. package/src/add/add-trigger.ts +154 -0
  120. package/src/add/add-variable.ts +9 -0
  121. package/src/add/add-workflow-graph.ts +180 -522
  122. package/src/add/add-workflow.ts +5 -4
  123. package/src/error-codes.ts +24 -1
  124. package/src/index.ts +22 -13
  125. package/src/inspector.ts +129 -17
  126. package/src/schema-generator.ts +1 -0
  127. package/src/types-map.ts +12 -1
  128. package/src/types.ts +175 -58
  129. package/src/utils/compute-required-schemas.ts +49 -0
  130. package/src/utils/contract-hashes.test.ts +528 -0
  131. package/src/utils/contract-hashes.ts +290 -0
  132. package/src/utils/custom-types-generator.ts +88 -0
  133. package/src/utils/detect-schema-vendor.ts +90 -0
  134. package/src/utils/ensure-function-metadata.ts +324 -7
  135. package/src/utils/extract-function-name.ts +101 -351
  136. package/src/utils/extract-services.ts +35 -2
  137. package/src/utils/filter-inspector-state.test.ts +34 -20
  138. package/src/utils/filter-inspector-state.ts +140 -31
  139. package/src/utils/get-property-value.ts +42 -4
  140. package/src/utils/hash.ts +26 -0
  141. package/src/utils/middleware.test.ts +204 -0
  142. package/src/utils/middleware.ts +129 -67
  143. package/src/utils/permissions.test.ts +35 -12
  144. package/src/utils/permissions.ts +10 -10
  145. package/src/utils/post-process.ts +283 -43
  146. package/src/utils/resolve-external-package.ts +42 -0
  147. package/src/utils/resolve-function-types.ts +42 -0
  148. package/src/utils/resolve-identifier.ts +46 -0
  149. package/src/utils/resolve-versions.test.ts +249 -0
  150. package/src/utils/resolve-versions.ts +105 -0
  151. package/src/utils/schema-generator.ts +329 -0
  152. package/src/utils/serialize-inspector-state.ts +163 -40
  153. package/src/utils/serialize-mcp-json.ts +145 -0
  154. package/src/utils/serialize-middleware-groups-meta.ts +33 -0
  155. package/src/utils/serialize-openapi-json.ts +277 -0
  156. package/src/utils/serialize-permissions-groups-meta.ts +35 -0
  157. package/src/utils/test-data/inspector-state.json +69 -66
  158. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +43 -119
  159. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +24 -4
  160. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +17 -10
  161. package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
  162. package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
  163. package/src/utils/workflow/graph/index.ts +5 -0
  164. package/src/utils/workflow/graph/serialize-workflow-graph.ts +1 -8
  165. package/src/utils/workflow/graph/workflow-graph.types.ts +29 -78
  166. package/src/visit.ts +12 -6
  167. package/tsconfig.tsbuildinfo +1 -1
  168. package/dist/add/add-forge-credential.d.ts +0 -8
  169. package/dist/add/add-forge-credential.js +0 -77
  170. package/dist/add/add-forge-node.d.ts +0 -7
  171. package/dist/add/add-forge-node.js +0 -77
  172. package/dist/add/add-mcp-tool.d.ts +0 -2
  173. package/dist/add/add-mcp-tool.js +0 -81
  174. package/dist/utils/extract-service-metadata.d.ts +0 -19
  175. package/dist/utils/extract-service-metadata.js +0 -244
  176. package/dist/utils/write-service-metadata.d.ts +0 -13
  177. package/dist/utils/write-service-metadata.js +0 -37
  178. package/src/add/add-forge-credential.ts +0 -119
  179. package/src/add/add-forge-node.ts +0 -132
  180. package/src/add/add-mcp-tool.ts +0 -141
  181. package/src/utils/extract-service-metadata.ts +0 -353
  182. package/src/utils/write-service-metadata.ts +0 -51
@@ -5,20 +5,110 @@ import {
5
5
  } from '../utils/get-property-value.js'
6
6
  import { pathToRegexp } from 'path-to-regexp'
7
7
  import { HTTPMethod } from '@pikku/core/http'
8
- import { extractFunctionName } from '../utils/extract-function-name.js'
9
- import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
10
- import { AddWiring } from '../types.js'
8
+ import {
9
+ extractFunctionName,
10
+ makeContextBasedId,
11
+ } from '../utils/extract-function-name.js'
12
+ import {
13
+ getPropertyAssignmentInitializer,
14
+ extractTypeKeys,
15
+ } from '../utils/type-utils.js'
16
+ import { AddWiring, InspectorState } from '../types.js'
11
17
  import { resolveHTTPMiddlewareFromObject } from '../utils/middleware.js'
12
18
  import { resolveHTTPPermissionsFromObject } from '../utils/permissions.js'
13
19
  import { extractWireNames } from '../utils/post-process.js'
14
20
  import { ensureFunctionMetadata } from '../utils/ensure-function-metadata.js'
15
21
  import { ErrorCode } from '../error-codes.js'
22
+ import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
23
+
24
+ import type { InspectorLogger } from '../types.js'
25
+
26
+ /**
27
+ * Parameters for registering an HTTP route
28
+ */
29
+ export interface RegisterHTTPRouteParams {
30
+ obj: ts.ObjectLiteralExpression
31
+ state: InspectorState
32
+ checker: ts.TypeChecker
33
+ logger: InspectorLogger
34
+ sourceFile: ts.SourceFile
35
+ basePath?: string
36
+ inheritedTags?: string[]
37
+ }
38
+
39
+ /**
40
+ * Extract header schema reference from headers property
41
+ */
42
+ const extractHeadersSchema = (
43
+ obj: ts.ObjectLiteralExpression,
44
+ routeName: string,
45
+ method: string,
46
+ state: InspectorState,
47
+ checker: ts.TypeChecker,
48
+ logger: InspectorLogger
49
+ ): string | undefined => {
50
+ const headersNode = getPropertyAssignmentInitializer(
51
+ obj,
52
+ 'headers',
53
+ true,
54
+ checker
55
+ )
56
+ if (!headersNode || !ts.isIdentifier(headersNode)) return undefined
57
+
58
+ // Resolve the schema reference
59
+ const symbol = checker.getSymbolAtLocation(headersNode)
60
+ if (!symbol) return undefined
61
+
62
+ const decl = symbol.valueDeclaration || symbol.declarations?.[0]
63
+ if (!decl) return undefined
64
+
65
+ let sourceFile: string
66
+ if (ts.isImportSpecifier(decl)) {
67
+ const aliasedSymbol = checker.getAliasedSymbol(symbol)
68
+ if (aliasedSymbol) {
69
+ const aliasedDecl =
70
+ aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0]
71
+ if (aliasedDecl) {
72
+ sourceFile = aliasedDecl.getSourceFile().fileName
73
+ } else {
74
+ return undefined
75
+ }
76
+ } else {
77
+ return undefined
78
+ }
79
+ } else {
80
+ sourceFile = decl.getSourceFile().fileName
81
+ }
82
+
83
+ const vendor = detectSchemaVendorOrError(
84
+ headersNode,
85
+ checker,
86
+ logger,
87
+ `Route '${routeName}' header`,
88
+ sourceFile
89
+ )
90
+ if (!vendor) return undefined
91
+
92
+ // Create a sanitized schema name from route and method to avoid collisions
93
+ const sanitizedRoute = routeName
94
+ .replace(/[^a-zA-Z0-9]/g, '_')
95
+ .replace(/^_+|_+$/g, '')
96
+ const schemaName = `${method.toUpperCase()}_${sanitizedRoute}_Headers`
97
+
98
+ state.schemaLookup.set(schemaName, {
99
+ variableName: headersNode.text,
100
+ sourceFile,
101
+ vendor,
102
+ })
103
+
104
+ return schemaName
105
+ }
16
106
 
17
107
  /**
18
108
  * Populate metaInputTypes for a given route based on method, input type,
19
- * query and params. Returns undefined (we only mutate metaTypes).
109
+ * query and params.
20
110
  */
21
- export const getInputTypes = (
111
+ const computeInputTypes = (
22
112
  metaTypes: Map<
23
113
  string,
24
114
  { query?: string[]; params?: string[]; body?: string[] }
@@ -27,7 +117,7 @@ export const getInputTypes = (
27
117
  inputType: string | null,
28
118
  queryValues: string[],
29
119
  paramsValues: string[]
30
- ): undefined => {
120
+ ): void => {
31
121
  if (!inputType) return
32
122
  metaTypes.set(inputType, {
33
123
  query: queryValues,
@@ -36,72 +126,60 @@ export const getInputTypes = (
36
126
  ? [...new Set([...queryValues, ...paramsValues])]
37
127
  : [],
38
128
  })
39
- return
40
129
  }
41
130
 
42
131
  /**
43
- * Simplified wireHTTP: re-uses function metadata from state.functions.meta
44
- * instead of re-inferring types here.
132
+ * Shared function to register an HTTP route in the inspector state.
133
+ * Used by both wireHTTP and wireHTTPRoutes.
45
134
  */
46
- export const addHTTPRoute: AddWiring = (
47
- logger,
48
- node,
49
- checker,
135
+ export function registerHTTPRoute({
136
+ obj,
50
137
  state,
51
- options
52
- ) => {
53
- // only look at calls
54
- if (!ts.isCallExpression(node)) return
138
+ checker,
139
+ logger,
140
+ sourceFile,
141
+ basePath = '',
142
+ inheritedTags = [],
143
+ }: RegisterHTTPRouteParams): void {
144
+ // Extract route path
145
+ const routePath = getPropertyValue(obj, 'route') as string | null
146
+ if (!routePath) return
55
147
 
56
- const { expression, arguments: args } = node
57
- if (!ts.isIdentifier(expression) || expression.text !== 'wireHTTP') return
148
+ const method = (
149
+ (getPropertyValue(obj, 'method') as string) || 'get'
150
+ ).toLowerCase()
151
+ const fullRoute = basePath + routePath
58
152
 
59
- // must pass an object literal
60
- const firstArg = args[0]
61
- if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
62
- const obj = firstArg
153
+ // Extract params from route path
154
+ let params: string[] = []
155
+ try {
156
+ const keys = pathToRegexp(fullRoute).keys
157
+ params = keys.filter((k) => k.type === 'param').map((k) => k.name)
158
+ } catch (e) {
159
+ logger.error(
160
+ `Failed to parse route '${fullRoute}': ${e instanceof Error ? e.message : e}`
161
+ )
162
+ return
163
+ }
63
164
 
64
- // --- extract HTTP metadata ---
65
- const route = getPropertyValue(obj, 'route') as string | null
66
- if (!route) return
165
+ // Get common metadata
166
+ const {
167
+ disabled,
168
+ title,
169
+ tags: routeTags,
170
+ summary,
171
+ description,
172
+ errors,
173
+ } = getCommonWireMetaData(obj, 'HTTP route', fullRoute, logger)
67
174
 
68
- const keys = pathToRegexp(route).keys
69
- const params = keys.filter((k) => k.type === 'param').map((k) => k.name)
175
+ if (disabled) return
70
176
 
71
- const method =
72
- (getPropertyValue(obj, 'method') as string)?.toLowerCase() || 'get'
73
- const { title, tags, summary, description, errors } = getCommonWireMetaData(
74
- obj,
75
- 'HTTP route',
76
- route,
77
- logger
78
- )
79
- const query = (getPropertyValue(obj, 'query') as string[]) || []
177
+ // Merge inherited tags with route tags
178
+ const tags = [...inheritedTags, ...(routeTags || [])]
80
179
 
81
- // Check if this is a workflow trigger (workflow: true)
82
- const isWorkflowTrigger = getPropertyValue(obj, 'workflow') === true
83
- if (isWorkflowTrigger) {
84
- // Workflow triggers don't need func - they're handled by workflow-utils
85
- // Just record the route for HTTP meta but skip function processing
86
- state.http.files.add(node.getSourceFile().fileName)
87
- state.http.meta[method][route] = {
88
- pikkuFuncName: '', // No function - workflow handles it
89
- route,
90
- method: method as HTTPMethod,
91
- params: params.length > 0 ? params : undefined,
92
- query: query.length > 0 ? query : undefined,
93
- inputTypes: undefined,
94
- title,
95
- summary,
96
- description,
97
- errors,
98
- tags,
99
- workflow: true,
100
- }
101
- return
102
- }
180
+ const query = (getPropertyValue(obj, 'query') as string[]) || []
103
181
 
104
- // --- find the referenced function name first for filtering ---
182
+ // Get function reference
105
183
  const funcInitializer = getPropertyAssignmentInitializer(
106
184
  obj,
107
185
  'func',
@@ -111,21 +189,27 @@ export const addHTTPRoute: AddWiring = (
111
189
  if (!funcInitializer) {
112
190
  logger.critical(
113
191
  ErrorCode.MISSING_FUNC,
114
- `No valid 'func' property for route '${route}'.`
192
+ `No valid 'func' property for route '${fullRoute}'.`
115
193
  )
116
194
  return
117
195
  }
118
196
 
119
- const funcName = extractFunctionName(
197
+ const extracted = extractFunctionName(funcInitializer, checker, state.rootDir)
198
+ let funcName = extracted.pikkuFuncId
199
+ if (funcName.startsWith('__temp_')) {
200
+ funcName = makeContextBasedId('http', method, fullRoute)
201
+ }
202
+
203
+ ensureFunctionMetadata(
204
+ state,
205
+ funcName,
206
+ fullRoute,
120
207
  funcInitializer,
121
208
  checker,
122
- state.rootDir
123
- ).pikkuFuncName
124
-
125
- // Ensure function metadata exists (creates stub for inline functions)
126
- ensureFunctionMetadata(state, funcName, route)
209
+ extracted.isHelper
210
+ )
127
211
 
128
- // lookup existing function metadata
212
+ // Lookup existing function metadata
129
213
  const fnMeta = state.functions.meta[funcName]
130
214
  if (!fnMeta) {
131
215
  logger.critical(
@@ -136,34 +220,64 @@ export const addHTTPRoute: AddWiring = (
136
220
  }
137
221
  const input = fnMeta.inputs?.[0] || null
138
222
 
139
- // --- compute inputTypes (body/query/params) ---
140
- const inputTypes = getInputTypes(
141
- state.http.metaInputTypes,
142
- method,
143
- input,
144
- query,
145
- params
146
- )
223
+ // Validate that route params and query params exist in function input type
224
+ if (params.length > 0 || query.length > 0) {
225
+ const inputTypes = state.typesLookup.get(funcName)
226
+ if (inputTypes && inputTypes.length > 0) {
227
+ const inputKeys = extractTypeKeys(inputTypes[0])
228
+
229
+ // Check path params
230
+ if (params.length > 0) {
231
+ const missingParams = params.filter((p) => !inputKeys.includes(p))
232
+ if (missingParams.length > 0) {
233
+ logger.critical(
234
+ ErrorCode.ROUTE_PARAM_MISMATCH,
235
+ `Route '${fullRoute}' has path parameter(s) [${missingParams.join(', ')}] ` +
236
+ `not found in function '${funcName}' input type. ` +
237
+ `Input type has: [${inputKeys.join(', ')}]`
238
+ )
239
+ return
240
+ }
241
+ }
242
+
243
+ // Check query params
244
+ if (query.length > 0) {
245
+ const missingQuery = query.filter((q) => !inputKeys.includes(q))
246
+ if (missingQuery.length > 0) {
247
+ logger.critical(
248
+ ErrorCode.ROUTE_QUERY_MISMATCH,
249
+ `Route '${fullRoute}' has query parameter(s) [${missingQuery.join(', ')}] ` +
250
+ `not found in function '${funcName}' input type. ` +
251
+ `Input type has: [${inputKeys.join(', ')}]`
252
+ )
253
+ return
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ // Compute inputTypes (body/query/params)
260
+ computeInputTypes(state.http.metaInputTypes, method, input, query, params)
147
261
 
148
- // --- resolve middleware ---
262
+ // Resolve middleware
149
263
  const middleware = resolveHTTPMiddlewareFromObject(
150
264
  state,
151
- route,
265
+ fullRoute,
152
266
  obj,
153
267
  tags,
154
268
  checker
155
269
  )
156
270
 
157
- // --- resolve permissions ---
271
+ // Resolve permissions
158
272
  const permissions = resolveHTTPPermissionsFromObject(
159
273
  state,
160
- route,
274
+ fullRoute,
161
275
  obj,
162
276
  tags,
163
277
  checker
164
278
  )
165
279
 
166
- // --- track used functions/middleware/permissions for service aggregation ---
280
+ // Track used functions/middleware/permissions for service aggregation
167
281
  state.serviceAggregation.usedFunctions.add(funcName)
168
282
  extractWireNames(middleware).forEach((name) =>
169
283
  state.serviceAggregation.usedMiddleware.add(name)
@@ -172,21 +286,64 @@ export const addHTTPRoute: AddWiring = (
172
286
  state.serviceAggregation.usedPermissions.add(name)
173
287
  )
174
288
 
175
- // --- record route ---
176
- state.http.files.add(node.getSourceFile().fileName)
177
- state.http.meta[method][route] = {
178
- pikkuFuncName: funcName,
179
- route,
289
+ // Check for SSE
290
+ const sse = getPropertyValue(obj, 'sse') === true
291
+
292
+ // Extract header schema
293
+ const headersSchemaName = extractHeadersSchema(
294
+ obj,
295
+ fullRoute,
296
+ method,
297
+ state,
298
+ checker,
299
+ logger
300
+ )
301
+
302
+ // Record route
303
+ state.http.files.add(sourceFile.fileName)
304
+ state.http.meta[method][fullRoute] = {
305
+ pikkuFuncId: funcName,
306
+ route: fullRoute,
180
307
  method: method as HTTPMethod,
181
308
  params: params.length > 0 ? params : undefined,
182
309
  query: query.length > 0 ? query : undefined,
183
- inputTypes,
310
+ inputTypes: undefined,
184
311
  title,
185
312
  summary,
186
313
  description,
187
314
  errors,
188
- tags,
315
+ tags: tags.length > 0 ? tags : undefined,
189
316
  middleware,
190
317
  permissions,
318
+ sse: sse ? true : undefined,
319
+ headersSchemaName,
320
+ groupBasePath: basePath || undefined,
191
321
  }
192
322
  }
323
+
324
+ /**
325
+ * Process wireHTTP calls
326
+ */
327
+ export const addHTTPRoute: AddWiring = (
328
+ logger,
329
+ node,
330
+ checker,
331
+ state,
332
+ _options
333
+ ) => {
334
+ if (!ts.isCallExpression(node)) return
335
+
336
+ const { expression, arguments: args } = node
337
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireHTTP') return
338
+
339
+ const firstArg = args[0]
340
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
341
+
342
+ registerHTTPRoute({
343
+ obj: firstArg,
344
+ state,
345
+ checker,
346
+ logger,
347
+ sourceFile: node.getSourceFile(),
348
+ })
349
+ }
@@ -0,0 +1,228 @@
1
+ import * as ts from 'typescript'
2
+ import { getPropertyValue } from '../utils/get-property-value.js'
3
+ import { AddWiring, InspectorState, InspectorLogger } from '../types.js'
4
+ import { registerHTTPRoute } from './add-http-route.js'
5
+ import { resolveIdentifier } from '../utils/resolve-identifier.js'
6
+
7
+ /**
8
+ * Group configuration extracted from wireHTTPRoutes or defineHTTPRoutes
9
+ */
10
+ interface GroupConfig {
11
+ basePath: string
12
+ tags: string[]
13
+ auth?: boolean
14
+ }
15
+
16
+ /**
17
+ * Process wireHTTPRoutes calls
18
+ */
19
+ export const addHTTPRoutes: AddWiring = (
20
+ logger,
21
+ node,
22
+ checker,
23
+ state,
24
+ _options
25
+ ) => {
26
+ if (!ts.isCallExpression(node)) return
27
+
28
+ const { expression, arguments: args } = node
29
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireHTTPRoutes')
30
+ return
31
+
32
+ const firstArg = args[0]
33
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
34
+
35
+ // Extract group config
36
+ const groupConfig = extractGroupConfig(firstArg)
37
+
38
+ // Get routes property
39
+ const routesProp = getPropertyAssignment(firstArg, 'routes')
40
+ if (!routesProp) return
41
+
42
+ // Process routes recursively
43
+ processRoutes(
44
+ routesProp.initializer,
45
+ groupConfig,
46
+ state,
47
+ checker,
48
+ logger,
49
+ node.getSourceFile()
50
+ )
51
+ }
52
+
53
+ /**
54
+ * Get a property assignment from an object literal
55
+ */
56
+ function getPropertyAssignment(
57
+ obj: ts.ObjectLiteralExpression,
58
+ propName: string
59
+ ): ts.PropertyAssignment | undefined {
60
+ for (const prop of obj.properties) {
61
+ if (
62
+ ts.isPropertyAssignment(prop) &&
63
+ ts.isIdentifier(prop.name) &&
64
+ prop.name.text === propName
65
+ ) {
66
+ return prop
67
+ }
68
+ }
69
+ return undefined
70
+ }
71
+
72
+ /**
73
+ * Extract group configuration from an object literal
74
+ */
75
+ function extractGroupConfig(obj: ts.ObjectLiteralExpression): GroupConfig {
76
+ const basePath = (getPropertyValue(obj, 'basePath') as string) || ''
77
+ const tags = (getPropertyValue(obj, 'tags') as string[]) || []
78
+ const auth = getPropertyValue(obj, 'auth')
79
+
80
+ return {
81
+ basePath,
82
+ tags,
83
+ auth: auth === true ? true : auth === false ? false : undefined,
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Merge two group configs following cascading rules
89
+ */
90
+ function mergeConfigs(parent: GroupConfig, child: GroupConfig): GroupConfig {
91
+ return {
92
+ basePath: parent.basePath + child.basePath,
93
+ tags: [...parent.tags, ...child.tags],
94
+ auth: child.auth ?? parent.auth,
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Check if a value is a route config (has method, func, and route)
100
+ */
101
+ function isRouteConfig(obj: ts.ObjectLiteralExpression): boolean {
102
+ let hasMethod = false
103
+ let hasFunc = false
104
+ let hasRoute = false
105
+
106
+ for (const prop of obj.properties) {
107
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
108
+ if (prop.name.text === 'method') hasMethod = true
109
+ if (prop.name.text === 'func') hasFunc = true
110
+ if (prop.name.text === 'route') hasRoute = true
111
+ }
112
+ }
113
+
114
+ return hasMethod && hasFunc && hasRoute
115
+ }
116
+
117
+ /**
118
+ * Check if a value is a route contract (has routes property but no method/func)
119
+ */
120
+ function isRouteContract(obj: ts.ObjectLiteralExpression): boolean {
121
+ let hasRoutes = false
122
+ let hasMethod = false
123
+ let hasFunc = false
124
+
125
+ for (const prop of obj.properties) {
126
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
127
+ if (prop.name.text === 'routes') hasRoutes = true
128
+ if (prop.name.text === 'method') hasMethod = true
129
+ if (prop.name.text === 'func') hasFunc = true
130
+ }
131
+ }
132
+
133
+ return hasRoutes && !hasMethod && !hasFunc
134
+ }
135
+
136
+ /**
137
+ * Recursively process routes - handles nested maps, contracts, and identifiers
138
+ */
139
+ function processRoutes(
140
+ node: ts.Node,
141
+ parentConfig: GroupConfig,
142
+ state: InspectorState,
143
+ checker: ts.TypeChecker,
144
+ logger: InspectorLogger,
145
+ sourceFile: ts.SourceFile
146
+ ): void {
147
+ // Handle array of routes
148
+ if (ts.isArrayLiteralExpression(node)) {
149
+ for (const element of node.elements) {
150
+ if (ts.isObjectLiteralExpression(element) && isRouteConfig(element)) {
151
+ processRoute(element, parentConfig, state, checker, logger, sourceFile)
152
+ }
153
+ }
154
+ return
155
+ }
156
+
157
+ // Handle object literal
158
+ if (ts.isObjectLiteralExpression(node)) {
159
+ // Check if this is a route config
160
+ if (isRouteConfig(node)) {
161
+ processRoute(node, parentConfig, state, checker, logger, sourceFile)
162
+ return
163
+ }
164
+
165
+ // Check if this is a route contract
166
+ if (isRouteContract(node)) {
167
+ const contractConfig = extractGroupConfig(node)
168
+ const mergedConfig = mergeConfigs(parentConfig, contractConfig)
169
+ const routesProp = getPropertyAssignment(node, 'routes')
170
+ if (routesProp) {
171
+ processRoutes(
172
+ routesProp.initializer,
173
+ mergedConfig,
174
+ state,
175
+ checker,
176
+ logger,
177
+ sourceFile
178
+ )
179
+ }
180
+ return
181
+ }
182
+
183
+ // Otherwise it's a nested map - process each property
184
+ for (const prop of node.properties) {
185
+ if (ts.isPropertyAssignment(prop)) {
186
+ processRoutes(
187
+ prop.initializer,
188
+ parentConfig,
189
+ state,
190
+ checker,
191
+ logger,
192
+ sourceFile
193
+ )
194
+ }
195
+ }
196
+ return
197
+ }
198
+
199
+ // Handle identifier - resolve to its definition
200
+ if (ts.isIdentifier(node)) {
201
+ const resolved = resolveIdentifier(node, checker, ['defineHTTPRoutes'])
202
+ if (resolved) {
203
+ processRoutes(resolved, parentConfig, state, checker, logger, sourceFile)
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Register a single route using the shared registerHTTPRoute function
210
+ */
211
+ function processRoute(
212
+ obj: ts.ObjectLiteralExpression,
213
+ groupConfig: GroupConfig,
214
+ state: InspectorState,
215
+ checker: ts.TypeChecker,
216
+ logger: InspectorLogger,
217
+ sourceFile: ts.SourceFile
218
+ ): void {
219
+ registerHTTPRoute({
220
+ obj,
221
+ state,
222
+ checker,
223
+ logger,
224
+ sourceFile,
225
+ basePath: groupConfig.basePath,
226
+ inheritedTags: groupConfig.tags,
227
+ })
228
+ }