@pikku/inspector 0.11.1 → 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 (189) hide show
  1. package/CHANGELOG.md +26 -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 +327 -59
  9. package/dist/add/add-http-route.d.ts +19 -10
  10. package/dist/add/add-http-route.js +153 -44
  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.d.ts +3 -0
  23. package/dist/add/add-rpc-invocations.js +65 -25
  24. package/dist/add/add-schedule.js +11 -5
  25. package/dist/add/add-secret.d.ts +3 -0
  26. package/dist/add/add-secret.js +82 -0
  27. package/dist/add/add-trigger.d.ts +2 -0
  28. package/dist/add/add-trigger.js +87 -0
  29. package/dist/add/add-variable.d.ts +1 -0
  30. package/dist/add/add-variable.js +8 -0
  31. package/dist/add/add-workflow-graph.d.ts +7 -0
  32. package/dist/add/add-workflow-graph.js +396 -0
  33. package/dist/add/add-workflow.js +124 -26
  34. package/dist/error-codes.d.ts +16 -1
  35. package/dist/error-codes.js +21 -1
  36. package/dist/index.d.ts +9 -5
  37. package/dist/index.js +5 -2
  38. package/dist/inspector.d.ts +1 -1
  39. package/dist/inspector.js +106 -13
  40. package/dist/schema-generator.d.ts +1 -0
  41. package/dist/schema-generator.js +1 -0
  42. package/dist/types-map.js +10 -1
  43. package/dist/types.d.ts +180 -30
  44. package/dist/utils/compute-required-schemas.d.ts +4 -0
  45. package/dist/utils/compute-required-schemas.js +41 -0
  46. package/dist/utils/contract-hashes.d.ts +35 -0
  47. package/dist/utils/contract-hashes.js +202 -0
  48. package/dist/utils/custom-types-generator.d.ts +9 -0
  49. package/dist/utils/custom-types-generator.js +71 -0
  50. package/dist/utils/detect-schema-vendor.d.ts +22 -0
  51. package/dist/utils/detect-schema-vendor.js +76 -0
  52. package/dist/utils/ensure-function-metadata.d.ts +5 -2
  53. package/dist/utils/ensure-function-metadata.js +220 -6
  54. package/dist/utils/extract-function-name.d.ts +5 -16
  55. package/dist/utils/extract-function-name.js +93 -298
  56. package/dist/utils/extract-services.d.ts +2 -1
  57. package/dist/utils/extract-services.js +25 -1
  58. package/dist/utils/filter-inspector-state.js +107 -23
  59. package/dist/utils/get-property-value.d.ts +8 -2
  60. package/dist/utils/get-property-value.js +33 -4
  61. package/dist/utils/hash.d.ts +2 -0
  62. package/dist/utils/hash.js +23 -0
  63. package/dist/utils/middleware.d.ts +7 -30
  64. package/dist/utils/middleware.js +80 -66
  65. package/dist/utils/permissions.d.ts +2 -2
  66. package/dist/utils/permissions.js +10 -10
  67. package/dist/utils/post-process.d.ts +9 -10
  68. package/dist/utils/post-process.js +231 -24
  69. package/dist/utils/resolve-external-package.d.ts +12 -0
  70. package/dist/utils/resolve-external-package.js +34 -0
  71. package/dist/utils/resolve-function-types.d.ts +6 -0
  72. package/dist/utils/resolve-function-types.js +29 -0
  73. package/dist/utils/resolve-identifier.d.ts +10 -0
  74. package/dist/utils/resolve-identifier.js +36 -0
  75. package/dist/utils/resolve-versions.d.ts +2 -0
  76. package/dist/utils/resolve-versions.js +78 -0
  77. package/dist/utils/schema-generator.d.ts +9 -0
  78. package/dist/utils/schema-generator.js +209 -0
  79. package/dist/utils/serialize-inspector-state.d.ts +73 -13
  80. package/dist/utils/serialize-inspector-state.js +102 -6
  81. package/dist/utils/serialize-mcp-json.d.ts +2 -0
  82. package/dist/utils/serialize-mcp-json.js +99 -0
  83. package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
  84. package/dist/utils/serialize-middleware-groups-meta.js +28 -0
  85. package/dist/utils/serialize-openapi-json.d.ts +85 -0
  86. package/dist/utils/serialize-openapi-json.js +151 -0
  87. package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
  88. package/dist/utils/serialize-permissions-groups-meta.js +31 -0
  89. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
  90. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +830 -0
  91. package/dist/{workflow/extract-simple-workflow.d.ts → utils/workflow/dsl/extract-dsl-workflow.d.ts} +4 -2
  92. package/dist/{workflow/extract-simple-workflow.js → utils/workflow/dsl/extract-dsl-workflow.js} +572 -72
  93. package/dist/utils/workflow/dsl/index.d.ts +7 -0
  94. package/dist/utils/workflow/dsl/index.js +7 -0
  95. package/dist/{workflow → utils/workflow/dsl}/patterns.d.ts +21 -0
  96. package/dist/{workflow → utils/workflow/dsl}/patterns.js +90 -10
  97. package/dist/{workflow → utils/workflow/dsl}/validation.d.ts +2 -0
  98. package/dist/{workflow → utils/workflow/dsl}/validation.js +25 -7
  99. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
  100. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +318 -0
  101. package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
  102. package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
  103. package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
  104. package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
  105. package/dist/utils/workflow/graph/index.d.ts +8 -0
  106. package/dist/utils/workflow/graph/index.js +8 -0
  107. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +35 -0
  108. package/dist/utils/workflow/graph/serialize-workflow-graph.js +150 -0
  109. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +203 -0
  110. package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
  111. package/dist/visit.js +13 -2
  112. package/package.json +26 -4
  113. package/src/add/add-ai-agent.ts +468 -0
  114. package/src/add/add-channel.ts +82 -79
  115. package/src/add/add-cli.ts +49 -20
  116. package/src/add/add-file-with-factory.ts +2 -0
  117. package/src/add/add-functions.ts +429 -71
  118. package/src/add/add-http-route.ts +246 -65
  119. package/src/add/add-http-routes.ts +228 -0
  120. package/src/add/add-keyed-wiring.ts +151 -0
  121. package/src/add/add-mcp-prompt.ts +26 -15
  122. package/src/add/add-mcp-resource.ts +27 -15
  123. package/src/add/add-middleware.ts +482 -80
  124. package/src/add/add-permission.ts +199 -40
  125. package/src/add/add-queue-worker.ts +24 -19
  126. package/src/add/add-rpc-invocations.ts +78 -31
  127. package/src/add/add-schedule.ts +16 -11
  128. package/src/add/add-secret.ts +140 -0
  129. package/src/add/add-trigger.ts +154 -0
  130. package/src/add/add-variable.ts +9 -0
  131. package/src/add/add-workflow-graph.ts +522 -0
  132. package/src/add/add-workflow.ts +117 -30
  133. package/src/error-codes.ts +26 -1
  134. package/src/index.ts +27 -8
  135. package/src/inspector.ts +145 -17
  136. package/src/schema-generator.ts +1 -0
  137. package/src/types-map.ts +12 -1
  138. package/src/types.ts +192 -51
  139. package/src/utils/compute-required-schemas.ts +49 -0
  140. package/src/utils/contract-hashes.test.ts +528 -0
  141. package/src/utils/contract-hashes.ts +290 -0
  142. package/src/utils/custom-types-generator.ts +88 -0
  143. package/src/utils/detect-schema-vendor.ts +90 -0
  144. package/src/utils/ensure-function-metadata.ts +324 -7
  145. package/src/utils/extract-function-name.ts +108 -358
  146. package/src/utils/extract-services.ts +35 -2
  147. package/src/utils/filter-inspector-state.test.ts +34 -20
  148. package/src/utils/filter-inspector-state.ts +140 -31
  149. package/src/utils/get-property-value.ts +50 -5
  150. package/src/utils/hash.ts +26 -0
  151. package/src/utils/middleware.test.ts +204 -0
  152. package/src/utils/middleware.ts +129 -67
  153. package/src/utils/permissions.test.ts +35 -12
  154. package/src/utils/permissions.ts +10 -10
  155. package/src/utils/post-process.ts +283 -43
  156. package/src/utils/resolve-external-package.ts +42 -0
  157. package/src/utils/resolve-function-types.ts +42 -0
  158. package/src/utils/resolve-identifier.ts +46 -0
  159. package/src/utils/resolve-versions.test.ts +249 -0
  160. package/src/utils/resolve-versions.ts +105 -0
  161. package/src/utils/schema-generator.ts +329 -0
  162. package/src/utils/serialize-inspector-state.ts +181 -20
  163. package/src/utils/serialize-mcp-json.ts +145 -0
  164. package/src/utils/serialize-middleware-groups-meta.ts +33 -0
  165. package/src/utils/serialize-openapi-json.ts +277 -0
  166. package/src/utils/serialize-permissions-groups-meta.ts +35 -0
  167. package/src/utils/test-data/inspector-state.json +69 -66
  168. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1104 -0
  169. package/src/{workflow/extract-simple-workflow.ts → utils/workflow/dsl/extract-dsl-workflow.ts} +678 -85
  170. package/src/utils/workflow/dsl/index.ts +11 -0
  171. package/src/{workflow → utils/workflow/dsl}/patterns.ts +108 -11
  172. package/src/{workflow → utils/workflow/dsl}/validation.ts +34 -7
  173. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +422 -0
  174. package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
  175. package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
  176. package/src/utils/workflow/graph/index.ts +11 -0
  177. package/src/utils/workflow/graph/serialize-workflow-graph.ts +216 -0
  178. package/src/utils/workflow/graph/workflow-graph.types.ts +231 -0
  179. package/src/visit.ts +14 -2
  180. package/tsconfig.tsbuildinfo +1 -1
  181. package/dist/add/add-mcp-tool.d.ts +0 -2
  182. package/dist/add/add-mcp-tool.js +0 -81
  183. package/dist/utils/extract-service-metadata.d.ts +0 -19
  184. package/dist/utils/extract-service-metadata.js +0 -244
  185. package/dist/utils/write-service-metadata.d.ts +0 -13
  186. package/dist/utils/write-service-metadata.js +0 -37
  187. package/src/add/add-mcp-tool.ts +0 -141
  188. package/src/utils/extract-service-metadata.ts +0 -353
  189. 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,49 +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
176
+
177
+ // Merge inherited tags with route tags
178
+ const tags = [...inheritedTags, ...(routeTags || [])]
70
179
 
71
- const method =
72
- (getPropertyValue(obj, 'method') as string)?.toLowerCase() || 'get'
73
- const { tags, summary, description, errors } = getCommonWireMetaData(
74
- obj,
75
- 'HTTP route',
76
- route,
77
- logger
78
- )
79
180
  const query = (getPropertyValue(obj, 'query') as string[]) || []
80
181
 
81
- // --- find the referenced function name first for filtering ---
182
+ // Get function reference
82
183
  const funcInitializer = getPropertyAssignmentInitializer(
83
184
  obj,
84
185
  'func',
@@ -88,21 +189,27 @@ export const addHTTPRoute: AddWiring = (
88
189
  if (!funcInitializer) {
89
190
  logger.critical(
90
191
  ErrorCode.MISSING_FUNC,
91
- `No valid 'func' property for route '${route}'.`
192
+ `No valid 'func' property for route '${fullRoute}'.`
92
193
  )
93
194
  return
94
195
  }
95
196
 
96
- 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,
97
207
  funcInitializer,
98
208
  checker,
99
- state.rootDir
100
- ).pikkuFuncName
101
-
102
- // Ensure function metadata exists (creates stub for inline functions)
103
- ensureFunctionMetadata(state, funcName, route)
209
+ extracted.isHelper
210
+ )
104
211
 
105
- // lookup existing function metadata
212
+ // Lookup existing function metadata
106
213
  const fnMeta = state.functions.meta[funcName]
107
214
  if (!fnMeta) {
108
215
  logger.critical(
@@ -113,34 +220,64 @@ export const addHTTPRoute: AddWiring = (
113
220
  }
114
221
  const input = fnMeta.inputs?.[0] || null
115
222
 
116
- // --- compute inputTypes (body/query/params) ---
117
- const inputTypes = getInputTypes(
118
- state.http.metaInputTypes,
119
- method,
120
- input,
121
- query,
122
- params
123
- )
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
+ }
124
242
 
125
- // --- resolve middleware ---
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)
261
+
262
+ // Resolve middleware
126
263
  const middleware = resolveHTTPMiddlewareFromObject(
127
264
  state,
128
- route,
265
+ fullRoute,
129
266
  obj,
130
267
  tags,
131
268
  checker
132
269
  )
133
270
 
134
- // --- resolve permissions ---
271
+ // Resolve permissions
135
272
  const permissions = resolveHTTPPermissionsFromObject(
136
273
  state,
137
- route,
274
+ fullRoute,
138
275
  obj,
139
276
  tags,
140
277
  checker
141
278
  )
142
279
 
143
- // --- track used functions/middleware/permissions for service aggregation ---
280
+ // Track used functions/middleware/permissions for service aggregation
144
281
  state.serviceAggregation.usedFunctions.add(funcName)
145
282
  extractWireNames(middleware).forEach((name) =>
146
283
  state.serviceAggregation.usedMiddleware.add(name)
@@ -149,20 +286,64 @@ export const addHTTPRoute: AddWiring = (
149
286
  state.serviceAggregation.usedPermissions.add(name)
150
287
  )
151
288
 
152
- // --- record route ---
153
- state.http.files.add(node.getSourceFile().fileName)
154
- state.http.meta[method][route] = {
155
- pikkuFuncName: funcName,
156
- 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,
157
307
  method: method as HTTPMethod,
158
308
  params: params.length > 0 ? params : undefined,
159
309
  query: query.length > 0 ? query : undefined,
160
- inputTypes,
310
+ inputTypes: undefined,
311
+ title,
161
312
  summary,
162
313
  description,
163
314
  errors,
164
- tags,
315
+ tags: tags.length > 0 ? tags : undefined,
165
316
  middleware,
166
317
  permissions,
318
+ sse: sse ? true : undefined,
319
+ headersSchemaName,
320
+ groupBasePath: basePath || undefined,
167
321
  }
168
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
+ }