@autoview/cli 0.1.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 (297) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +407 -0
  3. package/lib/AutoViewAgent.d.ts +109 -0
  4. package/lib/AutoViewAgent.js +123 -0
  5. package/lib/AutoViewAgent.js.map +1 -0
  6. package/lib/agent/emitMcpServer.d.ts +15 -0
  7. package/lib/agent/emitMcpServer.js +157 -0
  8. package/lib/agent/emitMcpServer.js.map +1 -0
  9. package/lib/agent/emitReport.d.ts +14 -0
  10. package/lib/agent/emitReport.js +85 -0
  11. package/lib/agent/emitReport.js.map +1 -0
  12. package/lib/agent/toolSurface.d.ts +130 -0
  13. package/lib/agent/toolSurface.js +342 -0
  14. package/lib/agent/toolSurface.js.map +1 -0
  15. package/lib/agent/verifyAgentTasks.d.ts +87 -0
  16. package/lib/agent/verifyAgentTasks.js +126 -0
  17. package/lib/agent/verifyAgentTasks.js.map +1 -0
  18. package/lib/cli/main.d.ts +2 -0
  19. package/lib/cli/main.js +295 -0
  20. package/lib/cli/main.js.map +1 -0
  21. package/lib/compiler/AutoViewInterfaceCompiler.d.ts +27 -0
  22. package/lib/compiler/AutoViewInterfaceCompiler.js +68 -0
  23. package/lib/compiler/AutoViewInterfaceCompiler.js.map +1 -0
  24. package/lib/constants/AutoViewFrontendTemplate.d.ts +1 -0
  25. package/lib/constants/AutoViewFrontendTemplate.js +46 -0
  26. package/lib/constants/AutoViewFrontendTemplate.js.map +1 -0
  27. package/lib/constants/AutoViewSystemPromptConstant.d.ts +5 -0
  28. package/lib/constants/AutoViewSystemPromptConstant.js +4 -0
  29. package/lib/constants/AutoViewSystemPromptConstant.js.map +1 -0
  30. package/lib/context/IAutoViewAgentContext.d.ts +60 -0
  31. package/lib/context/IAutoViewAgentContext.js +3 -0
  32. package/lib/context/IAutoViewAgentContext.js.map +1 -0
  33. package/lib/fromSwagger.d.ts +53 -0
  34. package/lib/fromSwagger.js +513 -0
  35. package/lib/fromSwagger.js.map +1 -0
  36. package/lib/generateDeterministic.d.ts +26 -0
  37. package/lib/generateDeterministic.js +75 -0
  38. package/lib/generateDeterministic.js.map +1 -0
  39. package/lib/index.d.ts +15 -0
  40. package/lib/index.js +41 -0
  41. package/lib/index.js.map +1 -0
  42. package/lib/orchestrate/orchestrateAutoView.d.ts +17 -0
  43. package/lib/orchestrate/orchestrateAutoView.js +491 -0
  44. package/lib/orchestrate/orchestrateAutoView.js.map +1 -0
  45. package/lib/orchestrate/orchestrateAutoViewProductPlan.d.ts +37 -0
  46. package/lib/orchestrate/orchestrateAutoViewProductPlan.js +109 -0
  47. package/lib/orchestrate/orchestrateAutoViewProductPlan.js.map +1 -0
  48. package/lib/orchestrate/orchestrateAutoViewRender.d.ts +133 -0
  49. package/lib/orchestrate/orchestrateAutoViewRender.js +943 -0
  50. package/lib/orchestrate/orchestrateAutoViewRender.js.map +1 -0
  51. package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.d.ts +24 -0
  52. package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js +92 -0
  53. package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js.map +1 -0
  54. package/lib/orchestrate/orchestrateAutoViewReview.d.ts +48 -0
  55. package/lib/orchestrate/orchestrateAutoViewReview.js +328 -0
  56. package/lib/orchestrate/orchestrateAutoViewReview.js.map +1 -0
  57. package/lib/orchestrate/orchestrateAutoViewScaffold.d.ts +45 -0
  58. package/lib/orchestrate/orchestrateAutoViewScaffold.js +586 -0
  59. package/lib/orchestrate/orchestrateAutoViewScaffold.js.map +1 -0
  60. package/lib/orchestrate/orchestrateAutoViewSdkStudy.d.ts +26 -0
  61. package/lib/orchestrate/orchestrateAutoViewSdkStudy.js +85 -0
  62. package/lib/orchestrate/orchestrateAutoViewSdkStudy.js.map +1 -0
  63. package/lib/orchestrate/structures/IAutoViewProductPlan.d.ts +96 -0
  64. package/lib/orchestrate/structures/IAutoViewProductPlan.js +3 -0
  65. package/lib/orchestrate/structures/IAutoViewProductPlan.js.map +1 -0
  66. package/lib/orchestrate/structures/IAutoViewProductPlanApplication.d.ts +38 -0
  67. package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js +3 -0
  68. package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js.map +1 -0
  69. package/lib/orchestrate/structures/IAutoViewRenderApplication.d.ts +38 -0
  70. package/lib/orchestrate/structures/IAutoViewRenderApplication.js +3 -0
  71. package/lib/orchestrate/structures/IAutoViewRenderApplication.js.map +1 -0
  72. package/lib/orchestrate/structures/IAutoViewReviewApplication.d.ts +40 -0
  73. package/lib/orchestrate/structures/IAutoViewReviewApplication.js +3 -0
  74. package/lib/orchestrate/structures/IAutoViewReviewApplication.js.map +1 -0
  75. package/lib/orchestrate/structures/IAutoViewSdkMap.d.ts +63 -0
  76. package/lib/orchestrate/structures/IAutoViewSdkMap.js +3 -0
  77. package/lib/orchestrate/structures/IAutoViewSdkMap.js.map +1 -0
  78. package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.d.ts +37 -0
  79. package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js +3 -0
  80. package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js.map +1 -0
  81. package/lib/orchestrate/utils/HistoryMessage.d.ts +10 -0
  82. package/lib/orchestrate/utils/HistoryMessage.js +25 -0
  83. package/lib/orchestrate/utils/HistoryMessage.js.map +1 -0
  84. package/lib/orchestrate/utils/auditFrontendRuntime.d.ts +53 -0
  85. package/lib/orchestrate/utils/auditFrontendRuntime.js +362 -0
  86. package/lib/orchestrate/utils/auditFrontendRuntime.js.map +1 -0
  87. package/lib/orchestrate/utils/buildDeterministicPlan.d.ts +4 -0
  88. package/lib/orchestrate/utils/buildDeterministicPlan.js +233 -0
  89. package/lib/orchestrate/utils/buildDeterministicPlan.js.map +1 -0
  90. package/lib/orchestrate/utils/buildDeterministicSdkMap.d.ts +22 -0
  91. package/lib/orchestrate/utils/buildDeterministicSdkMap.js +154 -0
  92. package/lib/orchestrate/utils/buildDeterministicSdkMap.js.map +1 -0
  93. package/lib/orchestrate/utils/cacheNodeModules.d.ts +31 -0
  94. package/lib/orchestrate/utils/cacheNodeModules.js +134 -0
  95. package/lib/orchestrate/utils/cacheNodeModules.js.map +1 -0
  96. package/lib/orchestrate/utils/describeEndpointPropsShape.d.ts +37 -0
  97. package/lib/orchestrate/utils/describeEndpointPropsShape.js +192 -0
  98. package/lib/orchestrate/utils/describeEndpointPropsShape.js.map +1 -0
  99. package/lib/orchestrate/utils/describeEndpointRequestBodyShape.d.ts +22 -0
  100. package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js +29 -0
  101. package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js.map +1 -0
  102. package/lib/orchestrate/utils/describeEndpointResponseShape.d.ts +19 -0
  103. package/lib/orchestrate/utils/describeEndpointResponseShape.js +30 -0
  104. package/lib/orchestrate/utils/describeEndpointResponseShape.js.map +1 -0
  105. package/lib/orchestrate/utils/executeCachedBatch.d.ts +22 -0
  106. package/lib/orchestrate/utils/executeCachedBatch.js +64 -0
  107. package/lib/orchestrate/utils/executeCachedBatch.js.map +1 -0
  108. package/lib/orchestrate/utils/loadShoppingFixture.d.ts +33 -0
  109. package/lib/orchestrate/utils/loadShoppingFixture.js +17 -0
  110. package/lib/orchestrate/utils/loadShoppingFixture.js.map +1 -0
  111. package/lib/orchestrate/utils/normalizeProductPlanPaths.d.ts +24 -0
  112. package/lib/orchestrate/utils/normalizeProductPlanPaths.js +77 -0
  113. package/lib/orchestrate/utils/normalizeProductPlanPaths.js.map +1 -0
  114. package/lib/orchestrate/utils/renderJsonSchema.d.ts +23 -0
  115. package/lib/orchestrate/utils/renderJsonSchema.js +122 -0
  116. package/lib/orchestrate/utils/renderJsonSchema.js.map +1 -0
  117. package/lib/orchestrate/utils/renderResourcePage.d.ts +36 -0
  118. package/lib/orchestrate/utils/renderResourcePage.js +1415 -0
  119. package/lib/orchestrate/utils/renderResourcePage.js.map +1 -0
  120. package/lib/orchestrate/utils/validateFrontendTypecheck.d.ts +109 -0
  121. package/lib/orchestrate/utils/validateFrontendTypecheck.js +274 -0
  122. package/lib/orchestrate/utils/validateFrontendTypecheck.js.map +1 -0
  123. package/lib/preview/renderPreview.d.ts +22 -0
  124. package/lib/preview/renderPreview.js +198 -0
  125. package/lib/preview/renderPreview.js.map +1 -0
  126. package/lib/typings/compiler.d.ts +39 -0
  127. package/lib/typings/compiler.js +3 -0
  128. package/lib/typings/compiler.js.map +1 -0
  129. package/lib/typings/events.d.ts +106 -0
  130. package/lib/typings/events.js +3 -0
  131. package/lib/typings/events.js.map +1 -0
  132. package/lib/typings/index.d.ts +10 -0
  133. package/lib/typings/index.js +27 -0
  134. package/lib/typings/index.js.map +1 -0
  135. package/lib/typings/misc.d.ts +78 -0
  136. package/lib/typings/misc.js +3 -0
  137. package/lib/typings/misc.js.map +1 -0
  138. package/lib/utils/ArrayUtil.d.ts +8 -0
  139. package/lib/utils/ArrayUtil.js +30 -0
  140. package/lib/utils/ArrayUtil.js.map +1 -0
  141. package/lib/utils/StringUtil.d.ts +11 -0
  142. package/lib/utils/StringUtil.js +28 -0
  143. package/lib/utils/StringUtil.js.map +1 -0
  144. package/lib/utils/classifyEndpoints.d.ts +62 -0
  145. package/lib/utils/classifyEndpoints.js +216 -0
  146. package/lib/utils/classifyEndpoints.js.map +1 -0
  147. package/lib/utils/endpointFilter.d.ts +26 -0
  148. package/lib/utils/endpointFilter.js +0 -0
  149. package/lib/utils/endpointFilter.js.map +1 -0
  150. package/lib/utils/extractFields.d.ts +85 -0
  151. package/lib/utils/extractFields.js +231 -0
  152. package/lib/utils/extractFields.js.map +1 -0
  153. package/lib/utils/index.d.ts +13 -0
  154. package/lib/utils/index.js +30 -0
  155. package/lib/utils/index.js.map +1 -0
  156. package/lib/utils/normalizeForNestia.d.ts +34 -0
  157. package/lib/utils/normalizeForNestia.js +133 -0
  158. package/lib/utils/normalizeForNestia.js.map +1 -0
  159. package/lib/utils/resourcePlan.d.ts +39 -0
  160. package/lib/utils/resourcePlan.js +95 -0
  161. package/lib/utils/resourcePlan.js.map +1 -0
  162. package/lib/utils/sliceDocument.d.ts +17 -0
  163. package/lib/utils/sliceDocument.js +114 -0
  164. package/lib/utils/sliceDocument.js.map +1 -0
  165. package/lib/utils/toEndpoints.d.ts +90 -0
  166. package/lib/utils/toEndpoints.js +227 -0
  167. package/lib/utils/toEndpoints.js.map +1 -0
  168. package/lib/verify/runWorkflows.d.ts +25 -0
  169. package/lib/verify/runWorkflows.js +366 -0
  170. package/lib/verify/runWorkflows.js.map +1 -0
  171. package/lib/verify/workflows.d.ts +53 -0
  172. package/lib/verify/workflows.js +107 -0
  173. package/lib/verify/workflows.js.map +1 -0
  174. package/package.json +82 -0
  175. package/prompts/AUTOVIEW_RENDER.md +398 -0
  176. package/prompts/AUTOVIEW_REVIEW.md +60 -0
  177. package/prompts/AUTOVIEW_SDK_STUDY.md +89 -0
  178. package/src/AutoViewAgent.ts +222 -0
  179. package/src/agent/emitMcpServer.integration.test.ts +168 -0
  180. package/src/agent/emitMcpServer.test.ts +51 -0
  181. package/src/agent/emitMcpServer.ts +178 -0
  182. package/src/agent/emitReport.ts +117 -0
  183. package/src/agent/toolSurface.test.ts +243 -0
  184. package/src/agent/toolSurface.ts +501 -0
  185. package/src/agent/verifyAgentTasks.test.ts +106 -0
  186. package/src/agent/verifyAgentTasks.ts +171 -0
  187. package/src/cli/main.ts +363 -0
  188. package/src/compiler/AutoViewInterfaceCompiler.ts +69 -0
  189. package/src/constants/AutoViewFrontendTemplate.ts +42 -0
  190. package/src/constants/AutoViewSystemPromptConstant.ts +6 -0
  191. package/src/context/IAutoViewAgentContext.ts +84 -0
  192. package/src/fromSwagger.test.ts +269 -0
  193. package/src/fromSwagger.ts +500 -0
  194. package/src/generateDeterministic.test.ts +39 -0
  195. package/src/generateDeterministic.ts +77 -0
  196. package/src/index.ts +30 -0
  197. package/src/orchestrate/orchestrateAutoView.ts +590 -0
  198. package/src/orchestrate/orchestrateAutoViewProductPlan.ts +121 -0
  199. package/src/orchestrate/orchestrateAutoViewRender.ts +1117 -0
  200. package/src/orchestrate/orchestrateAutoViewRenderDeterministic.ts +101 -0
  201. package/src/orchestrate/orchestrateAutoViewReview.ts +272 -0
  202. package/src/orchestrate/orchestrateAutoViewScaffold.ts +627 -0
  203. package/src/orchestrate/orchestrateAutoViewSdkStudy.ts +90 -0
  204. package/src/orchestrate/renderNavTs.test.ts +74 -0
  205. package/src/orchestrate/structures/IAutoViewProductPlan.ts +119 -0
  206. package/src/orchestrate/structures/IAutoViewProductPlanApplication.ts +41 -0
  207. package/src/orchestrate/structures/IAutoViewRenderApplication.ts +40 -0
  208. package/src/orchestrate/structures/IAutoViewReviewApplication.ts +42 -0
  209. package/src/orchestrate/structures/IAutoViewSdkMap.ts +72 -0
  210. package/src/orchestrate/structures/IAutoViewSdkStudyApplication.ts +40 -0
  211. package/src/orchestrate/utils/HistoryMessage.ts +41 -0
  212. package/src/orchestrate/utils/auditFrontendRuntime.test.ts +18 -0
  213. package/src/orchestrate/utils/auditFrontendRuntime.ts +454 -0
  214. package/src/orchestrate/utils/buildDeterministicPlan.test.ts +170 -0
  215. package/src/orchestrate/utils/buildDeterministicPlan.ts +289 -0
  216. package/src/orchestrate/utils/buildDeterministicSdkMap.test.ts +90 -0
  217. package/src/orchestrate/utils/buildDeterministicSdkMap.ts +169 -0
  218. package/src/orchestrate/utils/cacheNodeModules.ts +136 -0
  219. package/src/orchestrate/utils/describeEndpointPropsShape.test.ts +86 -0
  220. package/src/orchestrate/utils/describeEndpointPropsShape.ts +202 -0
  221. package/src/orchestrate/utils/describeEndpointRequestBodyShape.test.ts +87 -0
  222. package/src/orchestrate/utils/describeEndpointRequestBodyShape.ts +31 -0
  223. package/src/orchestrate/utils/describeEndpointResponseShape.test.ts +70 -0
  224. package/src/orchestrate/utils/describeEndpointResponseShape.ts +32 -0
  225. package/src/orchestrate/utils/executeCachedBatch.ts +59 -0
  226. package/src/orchestrate/utils/loadShoppingFixture.ts +52 -0
  227. package/src/orchestrate/utils/normalizeProductPlanPaths.ts +92 -0
  228. package/src/orchestrate/utils/renderJsonSchema.test.ts +162 -0
  229. package/src/orchestrate/utils/renderJsonSchema.ts +133 -0
  230. package/src/orchestrate/utils/renderResourcePage.test.ts +468 -0
  231. package/src/orchestrate/utils/renderResourcePage.ts +1624 -0
  232. package/src/orchestrate/utils/validateFrontendTypecheck.test.ts +32 -0
  233. package/src/orchestrate/utils/validateFrontendTypecheck.ts +335 -0
  234. package/src/preview/renderPreview.ts +273 -0
  235. package/src/typings/compiler.ts +47 -0
  236. package/src/typings/events.ts +155 -0
  237. package/src/typings/index.ts +10 -0
  238. package/src/typings/misc.ts +93 -0
  239. package/src/utils/ArrayUtil.ts +16 -0
  240. package/src/utils/StringUtil.ts +29 -0
  241. package/src/utils/classifyEndpoints.test.ts +86 -0
  242. package/src/utils/classifyEndpoints.ts +291 -0
  243. package/src/utils/endpointFilter.test.ts +50 -0
  244. package/src/utils/endpointFilter.ts +0 -0
  245. package/src/utils/extractFields.test.ts +82 -0
  246. package/src/utils/extractFields.ts +306 -0
  247. package/src/utils/index.ts +13 -0
  248. package/src/utils/normalizeForNestia.test.ts +93 -0
  249. package/src/utils/normalizeForNestia.ts +139 -0
  250. package/src/utils/resourcePlan.test.ts +104 -0
  251. package/src/utils/resourcePlan.ts +180 -0
  252. package/src/utils/sliceDocument.test.ts +85 -0
  253. package/src/utils/sliceDocument.ts +119 -0
  254. package/src/utils/toEndpoints.test.ts +251 -0
  255. package/src/utils/toEndpoints.ts +343 -0
  256. package/src/verify/runWorkflows.ts +403 -0
  257. package/src/verify/workflows.test.ts +117 -0
  258. package/src/verify/workflows.ts +154 -0
  259. package/template/CLAUDE.md +140 -0
  260. package/template/Dockerfile +31 -0
  261. package/template/PROMPT.md +80 -0
  262. package/template/SANDBOX.md +70 -0
  263. package/template/app/api/health/route.ts +10 -0
  264. package/template/app/globals.css +97 -0
  265. package/template/app/layout.tsx +30 -0
  266. package/template/app/page.tsx +19 -0
  267. package/template/components/AppShell.tsx +114 -0
  268. package/template/components/auto/CatalogGrid.tsx +159 -0
  269. package/template/components/auto/ConfirmButton.tsx +67 -0
  270. package/template/components/auto/EmbeddedCollection.tsx +144 -0
  271. package/template/components/auto/ResourceDashboard.tsx +104 -0
  272. package/template/components/auto/ResourceDetail.tsx +93 -0
  273. package/template/components/auto/ResourceForm.tsx +235 -0
  274. package/template/components/auto/ResourceIcon.tsx +88 -0
  275. package/template/components/auto/ResourceLanding.tsx +155 -0
  276. package/template/components/auto/ResourceTable.tsx +223 -0
  277. package/template/components/auto/formatValue.tsx +186 -0
  278. package/template/components/auto/types.ts +42 -0
  279. package/template/components/ui/badge.tsx +40 -0
  280. package/template/components/ui/button.tsx +57 -0
  281. package/template/components/ui/card.tsx +86 -0
  282. package/template/components/ui/dialog.tsx +119 -0
  283. package/template/components/ui/input.tsx +23 -0
  284. package/template/components/ui/label.tsx +24 -0
  285. package/template/components/ui/pagination.tsx +117 -0
  286. package/template/components/ui/select.tsx +92 -0
  287. package/template/components/ui/sheet.tsx +135 -0
  288. package/template/components/ui/skeleton.tsx +15 -0
  289. package/template/components/ui/table.tsx +120 -0
  290. package/template/components/ui/tabs.tsx +55 -0
  291. package/template/lib/utils.ts +35 -0
  292. package/template/next.config.mjs +52 -0
  293. package/template/package.json +46 -0
  294. package/template/postcss.config.js +6 -0
  295. package/template/scripts/start-shopping-backend.sh +56 -0
  296. package/template/tailwind.config.ts +96 -0
  297. package/template/tsconfig.json +29 -0
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { filterEndpoints } from "./endpointFilter";
4
+ import { IAutoViewEndpoint } from "./toEndpoints";
5
+
6
+ const ep = (path: string): IAutoViewEndpoint => ({
7
+ method: "get", path, accessor: path.split("/").filter(Boolean),
8
+ name: "x", description: "", parameters: [], query: null,
9
+ requestBody: null, responseBody: null,
10
+ });
11
+ const paths = (eps: IAutoViewEndpoint[]) => eps.map((e) => e.path);
12
+
13
+ describe("filterEndpoints", () => {
14
+ const all = [
15
+ ep("/shoppings/customers/sales"),
16
+ ep("/shoppings/customers/orders/{id}"),
17
+ ep("/shoppings/admins/coupons"),
18
+ ep("/shoppings/sellers/sales"),
19
+ ep("/monitors/health"),
20
+ ];
21
+
22
+ it("no filter is a no-op", () => {
23
+ expect(filterEndpoints(all)).toHaveLength(5);
24
+ });
25
+ it("include slices to one actor surface with ** across segments", () => {
26
+ expect(paths(filterEndpoints(all, { include: ["shoppings/customers/**"] }))).toEqual([
27
+ "/shoppings/customers/sales",
28
+ "/shoppings/customers/orders/{id}",
29
+ ]);
30
+ });
31
+ it("exclude drops a matching subtree", () => {
32
+ expect(paths(filterEndpoints(all, { exclude: ["monitors/**"] }))).not.toContain("/monitors/health");
33
+ });
34
+ it("include then exclude compose", () => {
35
+ const r = filterEndpoints(all, {
36
+ include: ["shoppings/**"],
37
+ exclude: ["**/admins/**"],
38
+ });
39
+ expect(paths(r)).not.toContain("/shoppings/admins/coupons");
40
+ expect(paths(r)).toContain("/shoppings/customers/sales");
41
+ });
42
+ it("single * stays within a segment", () => {
43
+ // `shoppings/*/sales` matches one segment, not customers/orders/...
44
+ const r = filterEndpoints(all, { include: ["shoppings/*/sales"] });
45
+ expect(paths(r).sort()).toEqual([
46
+ "/shoppings/customers/sales",
47
+ "/shoppings/sellers/sales",
48
+ ]);
49
+ });
50
+ });
Binary file
@@ -0,0 +1,82 @@
1
+ import { OpenApiConverter } from "@typia/utils";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { extractFields } from "./extractFields";
5
+
6
+ function doc(schemas: Record<string, unknown>) {
7
+ return OpenApiConverter.upgradeDocument({
8
+ openapi: "3.0.0",
9
+ info: { title: "t", version: "1.0.0" },
10
+ paths: {},
11
+ components: { schemas },
12
+ } as never);
13
+ }
14
+
15
+ describe("extractFields — deterministic data contract", () => {
16
+ it("extracts every property as a typed field with required flag", () => {
17
+ const d = doc({
18
+ Pet: {
19
+ type: "object",
20
+ required: ["name", "photoUrls"],
21
+ properties: {
22
+ id: { type: "integer" },
23
+ name: { type: "string" },
24
+ category: { $ref: "#/components/schemas/Category" },
25
+ photoUrls: { type: "array", items: { type: "string" } },
26
+ tags: { type: "array", items: { $ref: "#/components/schemas/Tag" } },
27
+ status: { type: "string", enum: ["available", "pending", "sold"] },
28
+ },
29
+ },
30
+ Category: { type: "object", properties: { id: { type: "integer" } } },
31
+ Tag: { type: "object", properties: { id: { type: "integer" } } },
32
+ });
33
+ const fields = extractFields("Pet", d);
34
+ const byName = Object.fromEntries(fields.map((f) => [f.name, f]));
35
+ expect(fields.map((f) => f.name)).toEqual(["id", "name", "category", "photoUrls", "tags", "status"]);
36
+ expect(byName.name!.required).toBe(true);
37
+ expect(byName.id!.required).toBe(false);
38
+ expect(byName.category!).toMatchObject({ kind: "ref", ref: "Category" });
39
+ expect(byName.photoUrls!).toMatchObject({ kind: "array", itemKind: "string" });
40
+ expect(byName.tags!).toMatchObject({ kind: "array", ref: "Tag" });
41
+ expect(byName.status!).toMatchObject({ kind: "enum" });
42
+ expect(byName.status!.enumValues).toEqual(["available", "pending", "sold"]);
43
+ });
44
+
45
+ it("resolves nullable refs, bare consts, and homogeneous unions", () => {
46
+ const d = doc({
47
+ Account: {
48
+ type: "object",
49
+ properties: {
50
+ // nullable ref → ref (not union)
51
+ member: { oneOf: [{ type: "null" }, { $ref: "#/components/schemas/Member" }] },
52
+ // bare discriminant const → one-value enum (not unknown)
53
+ type: { const: "account" },
54
+ // homogeneous primitive union (ipv4 | ipv6) → that primitive
55
+ ip: { oneOf: [{ type: "string", format: "ipv4" }, { type: "string", format: "ipv6" }] },
56
+ // genuine polymorphic ref union → stays union
57
+ owner: {
58
+ oneOf: [
59
+ { $ref: "#/components/schemas/Member" },
60
+ { $ref: "#/components/schemas/Guest" },
61
+ ],
62
+ },
63
+ },
64
+ },
65
+ Member: { type: "object", properties: { id: { type: "integer" } } },
66
+ Guest: { type: "object", properties: { id: { type: "integer" } } },
67
+ });
68
+ const byName = Object.fromEntries(
69
+ extractFields("Account", d).map((f) => [f.name, f]),
70
+ );
71
+ expect(byName.member!).toMatchObject({ kind: "ref", ref: "Member" });
72
+ expect(byName.type!).toMatchObject({ kind: "enum", enumValues: ["account"] });
73
+ expect(byName.ip!).toMatchObject({ kind: "string", format: "ipv4" });
74
+ expect(byName.owner!.kind).toBe("union");
75
+ });
76
+
77
+ it("returns [] for unknown or property-less types (graceful degrade)", () => {
78
+ const d = doc({ Free: { type: "object", additionalProperties: true } });
79
+ expect(extractFields("Free", d)).toEqual([]);
80
+ expect(extractFields("Missing", d)).toEqual([]);
81
+ });
82
+ });
@@ -0,0 +1,306 @@
1
+ import { OpenApi } from "@typia/interface";
2
+
3
+ /**
4
+ * Deterministic data contract — the field list a screen renders.
5
+ *
6
+ * Derived structurally from a component schema's properties: list table
7
+ * columns, detail rows, and form inputs all come from here instead of the LLM
8
+ * guessing. This is what makes "table 엄청 적게" structurally impossible — every
9
+ * property of the row/response/request type becomes a column/row/input.
10
+ */
11
+ export type FieldKind =
12
+ | "string"
13
+ | "number"
14
+ | "boolean"
15
+ | "enum"
16
+ | "array"
17
+ | "object"
18
+ | "ref"
19
+ | "union"
20
+ | "unknown";
21
+
22
+ export interface IFieldSpec {
23
+ name: string;
24
+ required: boolean;
25
+ kind: FieldKind;
26
+ /** String format (`uuid`, `date-time`, `uri`, ...) when present. */
27
+ format?: string;
28
+ /** Allowed values for an `enum` field. */
29
+ enumValues?: Array<string | number | boolean>;
30
+ /** Component type name for a `ref` field, or the element type of an `array`. */
31
+ ref?: string;
32
+ /** For an `array`, the element's kind (e.g. array of strings vs array of refs). */
33
+ itemKind?: FieldKind;
34
+ /** For an `array` of primitives, the element's string format (`uri`, …). A
35
+ * `string[]` of image URLs carries `format: "uri"` on the ITEM, not the
36
+ * array, so the signal is lost without preserving it here. */
37
+ itemFormat?: string;
38
+ /**
39
+ * For an `array` whose elements are an INLINE object (no `$ref`, so {@link ref}
40
+ * is undefined), the element's own field list — e.g. DigitalOcean's
41
+ * `{ droplets: [ { id, name, … } ] }`, where the droplet shape is inline. Lets
42
+ * a table render real columns for such collections instead of one opaque cell.
43
+ */
44
+ itemFields?: IFieldSpec[];
45
+ }
46
+
47
+ function refName($ref: string): string {
48
+ return $ref.split("/").pop()!;
49
+ }
50
+
51
+ /**
52
+ * Field names a paginated/collection response uses to hold its array of items.
53
+ * `data` is the nestia/AutoBE convention; real APIs vary (Box uses `entries`,
54
+ * others `items` / `results`). Recognizing all of them lets a list endpoint be
55
+ * rendered as a table off its element type instead of a one-record detail.
56
+ */
57
+ export const COLLECTION_FIELD_NAMES = new Set([
58
+ "data",
59
+ "entries",
60
+ "items",
61
+ "results",
62
+ "values",
63
+ "list",
64
+ "records",
65
+ "rows",
66
+ ]);
67
+
68
+ /** The array field that holds a collection response's items, if any. */
69
+ export function findCollectionField(
70
+ fields: IFieldSpec[],
71
+ ): IFieldSpec | undefined {
72
+ return fields.find(
73
+ (f) => f.kind === "array" && COLLECTION_FIELD_NAMES.has(f.name.toLowerCase()),
74
+ );
75
+ }
76
+
77
+ function kindOf(schema: OpenApi.IJsonSchema): {
78
+ kind: FieldKind;
79
+ format?: string;
80
+ enumValues?: Array<string | number | boolean>;
81
+ ref?: string;
82
+ itemKind?: FieldKind;
83
+ itemFormat?: string;
84
+ } {
85
+ if ("$ref" in schema && typeof schema.$ref === "string") {
86
+ return { kind: "ref", ref: refName(schema.$ref) };
87
+ }
88
+ // A bare `const` (no `oneOf`) is a single-value literal — typically a
89
+ // discriminant like `{ const: "customer" }`. Treat it as a one-value enum so
90
+ // it renders as a labeled value, not `unknown`.
91
+ if ("const" in schema && !("oneOf" in schema)) {
92
+ return {
93
+ kind: "enum",
94
+ enumValues: [(schema as { const: string | number | boolean }).const],
95
+ };
96
+ }
97
+ // upgradeDocument normalizes a string enum into `oneOf: [{ const }, ...]`, and
98
+ // a nullable type into `oneOf: [{ type: "null" }, <real schema>]`. Drop the
99
+ // null branch first, then: an all-`const` set is an enum; a single remaining
100
+ // branch is just that type made nullable (unwrap it — a nullable `$ref`
101
+ // becomes a `ref`, not a `union`); anything else is a real union.
102
+ if ("oneOf" in schema && Array.isArray(schema.oneOf)) {
103
+ const branches = schema.oneOf.filter(
104
+ (b) => !("type" in b && (b as { type?: unknown }).type === "null"),
105
+ );
106
+ const consts = branches.map((b) =>
107
+ "const" in b ? (b.const as string | number) : undefined,
108
+ );
109
+ if (consts.length > 0 && consts.every((c) => c !== undefined)) {
110
+ return { kind: "enum", enumValues: consts as Array<string | number> };
111
+ }
112
+ if (branches.length === 1) return kindOf(branches[0]!);
113
+ // A homogeneous primitive union (e.g. a string that is `ipv4 | ipv6`, or
114
+ // `uri | ""`) is, for display, just that primitive — collapse it. Branches
115
+ // with differing or no `type` (a real polymorphic `$ref` union) stay
116
+ // `union` so we never mislabel one variant as another.
117
+ const prims = branches.map((b) =>
118
+ "type" in b ? (b as { type?: unknown }).type : undefined,
119
+ );
120
+ if (prims.every((t) => t !== undefined && t === prims[0])) {
121
+ return kindOf(branches[0]!);
122
+ }
123
+ return { kind: "union" };
124
+ }
125
+ if ("anyOf" in schema || "allOf" in schema) {
126
+ return { kind: "union" };
127
+ }
128
+ if (!("type" in schema)) return { kind: "unknown" };
129
+ switch (schema.type) {
130
+ case "boolean":
131
+ return { kind: "boolean" };
132
+ case "integer":
133
+ case "number":
134
+ return { kind: "number" };
135
+ case "string": {
136
+ // biome-ignore lint: enum is part of the JSON Schema string node
137
+ const enumValues = (schema as { enum?: Array<string | number> }).enum;
138
+ if (Array.isArray(enumValues) && enumValues.length > 0) {
139
+ return { kind: "enum", enumValues, format: schema.format };
140
+ }
141
+ return { kind: "string", format: schema.format };
142
+ }
143
+ case "array": {
144
+ const items = "items" in schema ? schema.items : undefined;
145
+ if (items === undefined) return { kind: "array", itemKind: "unknown" };
146
+ const inner = kindOf(items);
147
+ return {
148
+ kind: "array",
149
+ itemKind: inner.kind,
150
+ ref: inner.ref,
151
+ itemFormat: inner.format,
152
+ };
153
+ }
154
+ case "object":
155
+ return { kind: "object" };
156
+ default:
157
+ return { kind: "unknown" };
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Resolve a schema's effective `{ properties, required }`, following `$ref` and
163
+ * merging every `allOf` member. Composition (`allOf`) is a core OpenAPI feature
164
+ * real swaggers (Box, Stripe, …) lean on heavily — a type is often
165
+ * `allOf: [BasePagination, { properties: { entries } }]` with no direct
166
+ * `properties` of its own. Without this, such a type renders zero fields.
167
+ *
168
+ * Own `properties` win over inherited ones on a name clash; `required` is the
169
+ * union. Depth-guarded against cyclic `$ref`.
170
+ */
171
+ export function resolveProperties(
172
+ schema: OpenApi.IJsonSchema,
173
+ doc: OpenApi.IDocument,
174
+ depth = 0,
175
+ ): { properties: Record<string, OpenApi.IJsonSchema>; required: string[] } {
176
+ if (depth > 8) return { properties: {}, required: [] };
177
+ if ("$ref" in schema && typeof schema.$ref === "string") {
178
+ const target = (doc.components?.schemas ?? {})[refName(schema.$ref)];
179
+ return target === undefined
180
+ ? { properties: {}, required: [] }
181
+ : resolveProperties(target, doc, depth + 1);
182
+ }
183
+ let properties: Record<string, OpenApi.IJsonSchema> = {};
184
+ let required: string[] = [];
185
+ if ("allOf" in schema && Array.isArray(schema.allOf)) {
186
+ for (const member of schema.allOf) {
187
+ const sub = resolveProperties(member, doc, depth + 1);
188
+ properties = { ...properties, ...sub.properties };
189
+ required = [...required, ...sub.required];
190
+ }
191
+ }
192
+ if ("properties" in schema && schema.properties !== undefined) {
193
+ properties = { ...properties, ...schema.properties };
194
+ }
195
+ if ("required" in schema && Array.isArray(schema.required)) {
196
+ required = [...required, ...schema.required];
197
+ }
198
+ return { properties, required: [...new Set(required)] };
199
+ }
200
+
201
+ /**
202
+ * Extract the field list of a schema directly (resolving `$ref` and `allOf`).
203
+ * Works for both named component schemas and INLINE schemas — e.g. a response
204
+ * `{ databases: T[], links: … }` that is not a named component (common on
205
+ * DigitalOcean). Returns `[]` when there are no resolvable `properties`.
206
+ */
207
+ export function extractFieldsFromSchema(
208
+ schema: OpenApi.IJsonSchema,
209
+ document: OpenApi.IDocument,
210
+ ): IFieldSpec[] {
211
+ const { properties, required } = resolveProperties(schema, document, 0);
212
+ const requiredSet = new Set(required);
213
+ return Object.entries(properties).map(([name, prop]) => {
214
+ const k = kindOf(prop);
215
+ // For an array of INLINE objects (no $ref element type), resolve the
216
+ // element's own fields so a table can show its columns.
217
+ let itemFields: IFieldSpec[] | undefined;
218
+ if (k.kind === "array" && k.ref === undefined) {
219
+ const items = (prop as { items?: OpenApi.IJsonSchema }).items;
220
+ if (items !== undefined) {
221
+ const inner = extractFieldsFromSchema(items, document);
222
+ if (inner.length > 0) itemFields = inner;
223
+ }
224
+ }
225
+ return {
226
+ name,
227
+ required: requiredSet.has(name),
228
+ kind: k.kind,
229
+ format: k.format,
230
+ enumValues: k.enumValues,
231
+ ref: k.ref,
232
+ itemKind: k.itemKind,
233
+ itemFormat: k.itemFormat,
234
+ itemFields,
235
+ };
236
+ });
237
+ }
238
+
239
+ /**
240
+ * Extract the field list of a NAMED component schema. Returns `[]` when the type
241
+ * is unknown or has no resolvable `properties` — the caller then degrades
242
+ * gracefully rather than inventing fields.
243
+ */
244
+ export function extractFields(
245
+ typeName: string,
246
+ document: OpenApi.IDocument,
247
+ ): IFieldSpec[] {
248
+ const schema = (document.components?.schemas ?? {})[typeName];
249
+ if (schema === undefined) return [];
250
+ return extractFieldsFromSchema(schema, document);
251
+ }
252
+
253
+ /** Field names that read as an image, optionally suffixed with url(s)/uri(s)/src. */
254
+ const IMAGE_NAME =
255
+ /(image|img|photo|picture|thumbnail|thumb|avatar|icon|logo|banner|cover|poster|artwork)s?(_?(url|uri|src|href)s?)?$/i;
256
+ /** Field names that hold a human title for a record (the card's heading). */
257
+ const TITLE_NAMES = new Set([
258
+ "name",
259
+ "title",
260
+ "label",
261
+ "headline",
262
+ "subject",
263
+ "nickname",
264
+ ]);
265
+
266
+ /**
267
+ * The field that best represents a record's image, or `undefined`. A scalar
268
+ * `string` (often `format: uri`) or a `string[]` of image URLs whose NAME reads
269
+ * as an image wins; failing that, a uri-formatted url-ish field. This is the
270
+ * signal that turns a list into a catalog of cards instead of a table.
271
+ */
272
+ export function imageFieldOf(columns: IFieldSpec[]): IFieldSpec | undefined {
273
+ const named = columns.find(
274
+ (c) =>
275
+ IMAGE_NAME.test(c.name) &&
276
+ (c.kind === "string" || (c.kind === "array" && c.itemKind === "string")),
277
+ );
278
+ if (named !== undefined) return named;
279
+ return columns.find(
280
+ (c) =>
281
+ (c.kind === "string" &&
282
+ c.format === "uri" &&
283
+ /url|uri|src|link/i.test(c.name)) ||
284
+ (c.kind === "array" && c.itemKind === "string" && c.itemFormat === "uri"),
285
+ );
286
+ }
287
+
288
+ /** The field that best titles a record (a card's heading), or `undefined`. */
289
+ export function titleFieldOf(columns: IFieldSpec[]): IFieldSpec | undefined {
290
+ return (
291
+ columns.find(
292
+ (c) => c.kind === "string" && TITLE_NAMES.has(c.name.toLowerCase()),
293
+ ) ?? columns.find((c) => c.kind === "string" && /name|title/i.test(c.name))
294
+ );
295
+ }
296
+
297
+ /**
298
+ * A collection deserves a catalog (card grid) rather than a table when its rows
299
+ * carry both an image and a title — the two things a card needs. Purely
300
+ * structural from the element schema; no domain keywords.
301
+ */
302
+ export function isCatalogCollection(columns: IFieldSpec[]): boolean {
303
+ return (
304
+ imageFieldOf(columns) !== undefined && titleFieldOf(columns) !== undefined
305
+ );
306
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Self-contained replacements for the `@autobe/utils` surface this package
3
+ * uses. Migrated 2026-06-01 as Step 2 of the AutoBE-independence track — the
4
+ * underlying conversions still rely on the typia / nestia ecosystem
5
+ * (`@typia/utils`, `@typia/interface`), which are not AutoBE packages.
6
+ */
7
+ export * from "./StringUtil";
8
+ export * from "./ArrayUtil";
9
+ export * from "./toEndpoints";
10
+ export * from "./classifyEndpoints";
11
+ export * from "./endpointFilter";
12
+ export * from "./extractFields";
13
+ export * from "./resourcePlan";
@@ -0,0 +1,93 @@
1
+ import { OpenApiConverter } from "@typia/utils";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { normalizeForNestia } from "./normalizeForNestia";
5
+
6
+ function doc(
7
+ paths: Record<string, unknown>,
8
+ schemas: Record<string, unknown> = {},
9
+ ) {
10
+ return OpenApiConverter.upgradeDocument({
11
+ openapi: "3.0.0",
12
+ info: { title: "t", version: "1.0.0" },
13
+ paths,
14
+ components: { schemas },
15
+ // biome-ignore lint: test fixture
16
+ } as never);
17
+ }
18
+
19
+ const op = (d: ReturnType<typeof doc>, path: string, method: string) =>
20
+ // biome-ignore lint: structural access to the normalized path item
21
+ (d.paths as Record<string, Record<string, { requestBody?: unknown; parameters?: { in: string }[] }>>)[
22
+ path
23
+ ]?.[method];
24
+
25
+ describe("normalizeForNestia", () => {
26
+ it("strips a requestBody from a GET operation (invalid HTTP nestia would mis-generate)", () => {
27
+ // Notion's `GET /v1/comments` declares an x-www-form-urlencoded requestBody.
28
+ // nestia faithfully emits a GET SDK function carrying a request body, which
29
+ // its own typed fetcher (body only on DELETE/POST/PUT/PATCH) then rejects.
30
+ const d = doc({
31
+ "/v1/comments": {
32
+ get: {
33
+ operationId: "comments",
34
+ requestBody: {
35
+ content: {
36
+ "application/x-www-form-urlencoded": {
37
+ schema: { type: "object", properties: { q: { type: "string" } } },
38
+ },
39
+ },
40
+ },
41
+ responses: { 200: { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/X" } } } } },
42
+ },
43
+ },
44
+ }, { X: { type: "object", properties: { id: { type: "string" } } } });
45
+ const out = normalizeForNestia(d);
46
+ expect(op(out, "/v1/comments", "get")?.requestBody).toBeUndefined();
47
+ });
48
+
49
+ it("keeps a requestBody on a POST operation (body is valid there)", () => {
50
+ const d = doc({
51
+ "/items": {
52
+ post: {
53
+ operationId: "create",
54
+ requestBody: { content: { "application/json": { schema: { type: "object", properties: { name: { type: "string" } } } } } },
55
+ responses: { 201: { description: "created", content: { "application/json": { schema: { $ref: "#/components/schemas/X" } } } } },
56
+ },
57
+ },
58
+ }, { X: { type: "object", properties: { id: { type: "string" } } } });
59
+ const out = normalizeForNestia(d);
60
+ expect(op(out, "/items", "post")?.requestBody).toBeDefined();
61
+ });
62
+
63
+ it("drops all but the first object-typed query param (the dropped-list fix)", () => {
64
+ const d = doc({
65
+ "/accounting/invoices": {
66
+ get: {
67
+ operationId: "invoicesAll",
68
+ parameters: [
69
+ { name: "filter", in: "query", schema: { $ref: "#/components/schemas/F" } },
70
+ { name: "sort", in: "query", schema: { type: "object", properties: { by: { type: "string" } } } },
71
+ { name: "limit", in: "query", schema: { type: "integer" } },
72
+ ],
73
+ responses: { 200: { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/X" } } } } },
74
+ },
75
+ },
76
+ }, {
77
+ X: { type: "object", properties: { id: { type: "string" } } },
78
+ F: { type: "object", properties: { n: { type: "string" } } },
79
+ });
80
+ const out = normalizeForNestia(d);
81
+ const params = op(out, "/accounting/invoices", "get")?.parameters ?? [];
82
+ const objectQuery = params.filter((p) => p.in === "query").length;
83
+ // filter (object) + limit (primitive) survive; sort (2nd object) is dropped.
84
+ expect(objectQuery).toBe(2);
85
+ });
86
+
87
+ it("returns the same document reference when nothing needs normalizing", () => {
88
+ const d = doc({
89
+ "/items": { get: { operationId: "list", responses: { 200: { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/X" } } } } } } },
90
+ }, { X: { type: "object", properties: { id: { type: "string" } } } });
91
+ expect(normalizeForNestia(d)).toBe(d);
92
+ });
93
+ });
@@ -0,0 +1,139 @@
1
+ import { OpenApi } from "@typia/interface";
2
+ import { OpenApiTypeChecker } from "@typia/utils";
3
+
4
+ /**
5
+ * Normalize a real-world OpenAPI document so nestia (`HttpMigration` and
6
+ * `@nestia/migrate`) can turn EVERY operation into a compilable typed route.
7
+ * Real swaggers carry shapes that nestia faithfully reproduces but its own typed
8
+ * fetcher then rejects (or refuses to emit), silently dropping screens and SDK
9
+ * functions. This pass rewrites only those shapes — additively, never removing a
10
+ * route that already worked — and is applied identically by the READ layer
11
+ * (`toEndpoints`) and the SDK compiler so the screen plan and the SDK stay in
12
+ * lockstep.
13
+ *
14
+ * Two corrections, both seen in the wild:
15
+ *
16
+ * 1. **Multi-object query → dropped route.** nestia drops an entire route when
17
+ * its query has more than one object-typed parameter
18
+ * (`query typed parameters must be only one object type`). Apideck's
19
+ * `GET /accounting/invoices` carries `filter`, `sort` AND `pass_through`
20
+ * objects — the very list that produces ids for navigation. We keep only the
21
+ * first object-typed query param; AutoView rebuilds the complete query from
22
+ * the untouched original document (`buildQuerySchema`) and the list page
23
+ * casts its query argument, so nothing the UI sees is lost.
24
+ *
25
+ * 2. **Body on a GET/HEAD → uncompilable SDK.** Notion's `GET /v1/comments` and
26
+ * `GET /v1/users/{id}` declare an `application/x-www-form-urlencoded`
27
+ * requestBody. A body on GET/HEAD is invalid HTTP; nestia still emits a GET
28
+ * SDK function carrying a request body, which its fetcher (body only on
29
+ * DELETE/POST/PUT/PATCH) rejects — a hard type error that also leaves those
30
+ * pages empty at runtime. We strip the requestBody from GET/HEAD operations.
31
+ *
32
+ * Pure: the input document is never mutated. Only operations that actually need
33
+ * a correction are rebuilt; everything else is shared by reference, and a
34
+ * document needing no correction is returned as-is (same reference).
35
+ */
36
+ export function normalizeForNestia(
37
+ document: OpenApi.IDocument,
38
+ ): OpenApi.IDocument {
39
+ const paths = document.paths ?? {};
40
+ let changed = false;
41
+ const nextPaths: Record<string, unknown> = {};
42
+ for (const [path, item] of Object.entries(paths)) {
43
+ const relaxed = normalizePathItem(item as Record<string, unknown>, document);
44
+ nextPaths[path] = relaxed.item;
45
+ changed = changed || relaxed.changed;
46
+ }
47
+ if (!changed) return document;
48
+ // biome-ignore lint: structurally an OpenApi.IDocument with a rebuilt paths map
49
+ return { ...document, paths: nextPaths } as OpenApi.IDocument;
50
+ }
51
+
52
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head"] as const;
53
+
54
+ /** HTTP methods whose request body nestia's typed fetcher cannot carry. */
55
+ const BODYLESS_METHODS = new Set(["get", "head"]);
56
+
57
+ function normalizePathItem(
58
+ item: Record<string, unknown>,
59
+ document: OpenApi.IDocument,
60
+ ): { item: Record<string, unknown>; changed: boolean } {
61
+ let changed = false;
62
+ const next: Record<string, unknown> = { ...item };
63
+ for (const method of HTTP_METHODS) {
64
+ const op = item[method] as OpenApi.IOperation | undefined;
65
+ if (op === undefined) continue;
66
+ const normalized = normalizeOperation(op, method, document);
67
+ if (normalized !== op) {
68
+ next[method] = normalized;
69
+ changed = true;
70
+ }
71
+ }
72
+ return changed ? { item: next, changed } : { item, changed: false };
73
+ }
74
+
75
+ function normalizeOperation(
76
+ operation: OpenApi.IOperation,
77
+ method: string,
78
+ document: OpenApi.IDocument,
79
+ ): OpenApi.IOperation {
80
+ let next = operation;
81
+ next = stripBodyFromBodylessMethod(next, method);
82
+ next = relaxMultiObjectQuery(next, document);
83
+ return next;
84
+ }
85
+
86
+ /**
87
+ * Remove the requestBody from a GET/HEAD operation — a body is invalid there and
88
+ * makes nestia emit an SDK function its own fetcher rejects.
89
+ */
90
+ function stripBodyFromBodylessMethod(
91
+ operation: OpenApi.IOperation,
92
+ method: string,
93
+ ): OpenApi.IOperation {
94
+ if (!BODYLESS_METHODS.has(method)) return operation;
95
+ if (operation.requestBody === undefined) return operation;
96
+ const { requestBody: _dropped, ...rest } = operation as OpenApi.IOperation & {
97
+ requestBody?: unknown;
98
+ };
99
+ return rest as OpenApi.IOperation;
100
+ }
101
+
102
+ /**
103
+ * Keep only the first object-typed query parameter, or return the operation
104
+ * unchanged when it has ≤1 such param.
105
+ */
106
+ function relaxMultiObjectQuery(
107
+ operation: OpenApi.IOperation,
108
+ document: OpenApi.IDocument,
109
+ ): OpenApi.IOperation {
110
+ const params = operation.parameters;
111
+ if (!Array.isArray(params)) return operation;
112
+ const objectQueryIndexes = params
113
+ .map((p, i) =>
114
+ p.in === "query" && isObjectQuerySchema(p.schema, document) ? i : -1,
115
+ )
116
+ .filter((i) => i >= 0);
117
+ if (objectQueryIndexes.length <= 1) return operation;
118
+ const drop = new Set(objectQueryIndexes.slice(1));
119
+ return { ...operation, parameters: params.filter((_, i) => !drop.has(i)) };
120
+ }
121
+
122
+ /**
123
+ * Mirror of the composer's object test (HttpMigrateRouteComposer): a query param
124
+ * counts as an "object" when its schema is an object, or a `$ref` to a component
125
+ * schema that is an object.
126
+ */
127
+ function isObjectQuerySchema(
128
+ schema: OpenApi.IJsonSchema | undefined,
129
+ document: OpenApi.IDocument,
130
+ ): boolean {
131
+ if (schema === undefined) return false;
132
+ if (OpenApiTypeChecker.isObject(schema)) return true;
133
+ if (OpenApiTypeChecker.isReference(schema)) {
134
+ const name = schema.$ref.split("/").pop() ?? "";
135
+ const target = (document.components?.schemas ?? {})[name];
136
+ return target !== undefined && OpenApiTypeChecker.isObject(target);
137
+ }
138
+ return false;
139
+ }