@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.
- package/LICENSE +661 -0
- package/README.md +407 -0
- package/lib/AutoViewAgent.d.ts +109 -0
- package/lib/AutoViewAgent.js +123 -0
- package/lib/AutoViewAgent.js.map +1 -0
- package/lib/agent/emitMcpServer.d.ts +15 -0
- package/lib/agent/emitMcpServer.js +157 -0
- package/lib/agent/emitMcpServer.js.map +1 -0
- package/lib/agent/emitReport.d.ts +14 -0
- package/lib/agent/emitReport.js +85 -0
- package/lib/agent/emitReport.js.map +1 -0
- package/lib/agent/toolSurface.d.ts +130 -0
- package/lib/agent/toolSurface.js +342 -0
- package/lib/agent/toolSurface.js.map +1 -0
- package/lib/agent/verifyAgentTasks.d.ts +87 -0
- package/lib/agent/verifyAgentTasks.js +126 -0
- package/lib/agent/verifyAgentTasks.js.map +1 -0
- package/lib/cli/main.d.ts +2 -0
- package/lib/cli/main.js +295 -0
- package/lib/cli/main.js.map +1 -0
- package/lib/compiler/AutoViewInterfaceCompiler.d.ts +27 -0
- package/lib/compiler/AutoViewInterfaceCompiler.js +68 -0
- package/lib/compiler/AutoViewInterfaceCompiler.js.map +1 -0
- package/lib/constants/AutoViewFrontendTemplate.d.ts +1 -0
- package/lib/constants/AutoViewFrontendTemplate.js +46 -0
- package/lib/constants/AutoViewFrontendTemplate.js.map +1 -0
- package/lib/constants/AutoViewSystemPromptConstant.d.ts +5 -0
- package/lib/constants/AutoViewSystemPromptConstant.js +4 -0
- package/lib/constants/AutoViewSystemPromptConstant.js.map +1 -0
- package/lib/context/IAutoViewAgentContext.d.ts +60 -0
- package/lib/context/IAutoViewAgentContext.js +3 -0
- package/lib/context/IAutoViewAgentContext.js.map +1 -0
- package/lib/fromSwagger.d.ts +53 -0
- package/lib/fromSwagger.js +513 -0
- package/lib/fromSwagger.js.map +1 -0
- package/lib/generateDeterministic.d.ts +26 -0
- package/lib/generateDeterministic.js +75 -0
- package/lib/generateDeterministic.js.map +1 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +41 -0
- package/lib/index.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoView.d.ts +17 -0
- package/lib/orchestrate/orchestrateAutoView.js +491 -0
- package/lib/orchestrate/orchestrateAutoView.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.d.ts +37 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.js +109 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewRender.d.ts +133 -0
- package/lib/orchestrate/orchestrateAutoViewRender.js +943 -0
- package/lib/orchestrate/orchestrateAutoViewRender.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.d.ts +24 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js +92 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewReview.d.ts +48 -0
- package/lib/orchestrate/orchestrateAutoViewReview.js +328 -0
- package/lib/orchestrate/orchestrateAutoViewReview.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.d.ts +45 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.js +586 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.d.ts +26 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.js +85 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.d.ts +96 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.js +3 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.d.ts +38 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.d.ts +38 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.d.ts +40 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.d.ts +63 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.js +3 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.d.ts +37 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js.map +1 -0
- package/lib/orchestrate/utils/HistoryMessage.d.ts +10 -0
- package/lib/orchestrate/utils/HistoryMessage.js +25 -0
- package/lib/orchestrate/utils/HistoryMessage.js.map +1 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.d.ts +53 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.js +362 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.js.map +1 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.d.ts +4 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.js +233 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.js.map +1 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.d.ts +22 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.js +154 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.js.map +1 -0
- package/lib/orchestrate/utils/cacheNodeModules.d.ts +31 -0
- package/lib/orchestrate/utils/cacheNodeModules.js +134 -0
- package/lib/orchestrate/utils/cacheNodeModules.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.d.ts +37 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.js +192 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.d.ts +22 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js +29 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.d.ts +19 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.js +30 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.js.map +1 -0
- package/lib/orchestrate/utils/executeCachedBatch.d.ts +22 -0
- package/lib/orchestrate/utils/executeCachedBatch.js +64 -0
- package/lib/orchestrate/utils/executeCachedBatch.js.map +1 -0
- package/lib/orchestrate/utils/loadShoppingFixture.d.ts +33 -0
- package/lib/orchestrate/utils/loadShoppingFixture.js +17 -0
- package/lib/orchestrate/utils/loadShoppingFixture.js.map +1 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.d.ts +24 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.js +77 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.js.map +1 -0
- package/lib/orchestrate/utils/renderJsonSchema.d.ts +23 -0
- package/lib/orchestrate/utils/renderJsonSchema.js +122 -0
- package/lib/orchestrate/utils/renderJsonSchema.js.map +1 -0
- package/lib/orchestrate/utils/renderResourcePage.d.ts +36 -0
- package/lib/orchestrate/utils/renderResourcePage.js +1415 -0
- package/lib/orchestrate/utils/renderResourcePage.js.map +1 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.d.ts +109 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.js +274 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.js.map +1 -0
- package/lib/preview/renderPreview.d.ts +22 -0
- package/lib/preview/renderPreview.js +198 -0
- package/lib/preview/renderPreview.js.map +1 -0
- package/lib/typings/compiler.d.ts +39 -0
- package/lib/typings/compiler.js +3 -0
- package/lib/typings/compiler.js.map +1 -0
- package/lib/typings/events.d.ts +106 -0
- package/lib/typings/events.js +3 -0
- package/lib/typings/events.js.map +1 -0
- package/lib/typings/index.d.ts +10 -0
- package/lib/typings/index.js +27 -0
- package/lib/typings/index.js.map +1 -0
- package/lib/typings/misc.d.ts +78 -0
- package/lib/typings/misc.js +3 -0
- package/lib/typings/misc.js.map +1 -0
- package/lib/utils/ArrayUtil.d.ts +8 -0
- package/lib/utils/ArrayUtil.js +30 -0
- package/lib/utils/ArrayUtil.js.map +1 -0
- package/lib/utils/StringUtil.d.ts +11 -0
- package/lib/utils/StringUtil.js +28 -0
- package/lib/utils/StringUtil.js.map +1 -0
- package/lib/utils/classifyEndpoints.d.ts +62 -0
- package/lib/utils/classifyEndpoints.js +216 -0
- package/lib/utils/classifyEndpoints.js.map +1 -0
- package/lib/utils/endpointFilter.d.ts +26 -0
- package/lib/utils/endpointFilter.js +0 -0
- package/lib/utils/endpointFilter.js.map +1 -0
- package/lib/utils/extractFields.d.ts +85 -0
- package/lib/utils/extractFields.js +231 -0
- package/lib/utils/extractFields.js.map +1 -0
- package/lib/utils/index.d.ts +13 -0
- package/lib/utils/index.js +30 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/normalizeForNestia.d.ts +34 -0
- package/lib/utils/normalizeForNestia.js +133 -0
- package/lib/utils/normalizeForNestia.js.map +1 -0
- package/lib/utils/resourcePlan.d.ts +39 -0
- package/lib/utils/resourcePlan.js +95 -0
- package/lib/utils/resourcePlan.js.map +1 -0
- package/lib/utils/sliceDocument.d.ts +17 -0
- package/lib/utils/sliceDocument.js +114 -0
- package/lib/utils/sliceDocument.js.map +1 -0
- package/lib/utils/toEndpoints.d.ts +90 -0
- package/lib/utils/toEndpoints.js +227 -0
- package/lib/utils/toEndpoints.js.map +1 -0
- package/lib/verify/runWorkflows.d.ts +25 -0
- package/lib/verify/runWorkflows.js +366 -0
- package/lib/verify/runWorkflows.js.map +1 -0
- package/lib/verify/workflows.d.ts +53 -0
- package/lib/verify/workflows.js +107 -0
- package/lib/verify/workflows.js.map +1 -0
- package/package.json +82 -0
- package/prompts/AUTOVIEW_RENDER.md +398 -0
- package/prompts/AUTOVIEW_REVIEW.md +60 -0
- package/prompts/AUTOVIEW_SDK_STUDY.md +89 -0
- package/src/AutoViewAgent.ts +222 -0
- package/src/agent/emitMcpServer.integration.test.ts +168 -0
- package/src/agent/emitMcpServer.test.ts +51 -0
- package/src/agent/emitMcpServer.ts +178 -0
- package/src/agent/emitReport.ts +117 -0
- package/src/agent/toolSurface.test.ts +243 -0
- package/src/agent/toolSurface.ts +501 -0
- package/src/agent/verifyAgentTasks.test.ts +106 -0
- package/src/agent/verifyAgentTasks.ts +171 -0
- package/src/cli/main.ts +363 -0
- package/src/compiler/AutoViewInterfaceCompiler.ts +69 -0
- package/src/constants/AutoViewFrontendTemplate.ts +42 -0
- package/src/constants/AutoViewSystemPromptConstant.ts +6 -0
- package/src/context/IAutoViewAgentContext.ts +84 -0
- package/src/fromSwagger.test.ts +269 -0
- package/src/fromSwagger.ts +500 -0
- package/src/generateDeterministic.test.ts +39 -0
- package/src/generateDeterministic.ts +77 -0
- package/src/index.ts +30 -0
- package/src/orchestrate/orchestrateAutoView.ts +590 -0
- package/src/orchestrate/orchestrateAutoViewProductPlan.ts +121 -0
- package/src/orchestrate/orchestrateAutoViewRender.ts +1117 -0
- package/src/orchestrate/orchestrateAutoViewRenderDeterministic.ts +101 -0
- package/src/orchestrate/orchestrateAutoViewReview.ts +272 -0
- package/src/orchestrate/orchestrateAutoViewScaffold.ts +627 -0
- package/src/orchestrate/orchestrateAutoViewSdkStudy.ts +90 -0
- package/src/orchestrate/renderNavTs.test.ts +74 -0
- package/src/orchestrate/structures/IAutoViewProductPlan.ts +119 -0
- package/src/orchestrate/structures/IAutoViewProductPlanApplication.ts +41 -0
- package/src/orchestrate/structures/IAutoViewRenderApplication.ts +40 -0
- package/src/orchestrate/structures/IAutoViewReviewApplication.ts +42 -0
- package/src/orchestrate/structures/IAutoViewSdkMap.ts +72 -0
- package/src/orchestrate/structures/IAutoViewSdkStudyApplication.ts +40 -0
- package/src/orchestrate/utils/HistoryMessage.ts +41 -0
- package/src/orchestrate/utils/auditFrontendRuntime.test.ts +18 -0
- package/src/orchestrate/utils/auditFrontendRuntime.ts +454 -0
- package/src/orchestrate/utils/buildDeterministicPlan.test.ts +170 -0
- package/src/orchestrate/utils/buildDeterministicPlan.ts +289 -0
- package/src/orchestrate/utils/buildDeterministicSdkMap.test.ts +90 -0
- package/src/orchestrate/utils/buildDeterministicSdkMap.ts +169 -0
- package/src/orchestrate/utils/cacheNodeModules.ts +136 -0
- package/src/orchestrate/utils/describeEndpointPropsShape.test.ts +86 -0
- package/src/orchestrate/utils/describeEndpointPropsShape.ts +202 -0
- package/src/orchestrate/utils/describeEndpointRequestBodyShape.test.ts +87 -0
- package/src/orchestrate/utils/describeEndpointRequestBodyShape.ts +31 -0
- package/src/orchestrate/utils/describeEndpointResponseShape.test.ts +70 -0
- package/src/orchestrate/utils/describeEndpointResponseShape.ts +32 -0
- package/src/orchestrate/utils/executeCachedBatch.ts +59 -0
- package/src/orchestrate/utils/loadShoppingFixture.ts +52 -0
- package/src/orchestrate/utils/normalizeProductPlanPaths.ts +92 -0
- package/src/orchestrate/utils/renderJsonSchema.test.ts +162 -0
- package/src/orchestrate/utils/renderJsonSchema.ts +133 -0
- package/src/orchestrate/utils/renderResourcePage.test.ts +468 -0
- package/src/orchestrate/utils/renderResourcePage.ts +1624 -0
- package/src/orchestrate/utils/validateFrontendTypecheck.test.ts +32 -0
- package/src/orchestrate/utils/validateFrontendTypecheck.ts +335 -0
- package/src/preview/renderPreview.ts +273 -0
- package/src/typings/compiler.ts +47 -0
- package/src/typings/events.ts +155 -0
- package/src/typings/index.ts +10 -0
- package/src/typings/misc.ts +93 -0
- package/src/utils/ArrayUtil.ts +16 -0
- package/src/utils/StringUtil.ts +29 -0
- package/src/utils/classifyEndpoints.test.ts +86 -0
- package/src/utils/classifyEndpoints.ts +291 -0
- package/src/utils/endpointFilter.test.ts +50 -0
- package/src/utils/endpointFilter.ts +0 -0
- package/src/utils/extractFields.test.ts +82 -0
- package/src/utils/extractFields.ts +306 -0
- package/src/utils/index.ts +13 -0
- package/src/utils/normalizeForNestia.test.ts +93 -0
- package/src/utils/normalizeForNestia.ts +139 -0
- package/src/utils/resourcePlan.test.ts +104 -0
- package/src/utils/resourcePlan.ts +180 -0
- package/src/utils/sliceDocument.test.ts +85 -0
- package/src/utils/sliceDocument.ts +119 -0
- package/src/utils/toEndpoints.test.ts +251 -0
- package/src/utils/toEndpoints.ts +343 -0
- package/src/verify/runWorkflows.ts +403 -0
- package/src/verify/workflows.test.ts +117 -0
- package/src/verify/workflows.ts +154 -0
- package/template/CLAUDE.md +140 -0
- package/template/Dockerfile +31 -0
- package/template/PROMPT.md +80 -0
- package/template/SANDBOX.md +70 -0
- package/template/app/api/health/route.ts +10 -0
- package/template/app/globals.css +97 -0
- package/template/app/layout.tsx +30 -0
- package/template/app/page.tsx +19 -0
- package/template/components/AppShell.tsx +114 -0
- package/template/components/auto/CatalogGrid.tsx +159 -0
- package/template/components/auto/ConfirmButton.tsx +67 -0
- package/template/components/auto/EmbeddedCollection.tsx +144 -0
- package/template/components/auto/ResourceDashboard.tsx +104 -0
- package/template/components/auto/ResourceDetail.tsx +93 -0
- package/template/components/auto/ResourceForm.tsx +235 -0
- package/template/components/auto/ResourceIcon.tsx +88 -0
- package/template/components/auto/ResourceLanding.tsx +155 -0
- package/template/components/auto/ResourceTable.tsx +223 -0
- package/template/components/auto/formatValue.tsx +186 -0
- package/template/components/auto/types.ts +42 -0
- package/template/components/ui/badge.tsx +40 -0
- package/template/components/ui/button.tsx +57 -0
- package/template/components/ui/card.tsx +86 -0
- package/template/components/ui/dialog.tsx +119 -0
- package/template/components/ui/input.tsx +23 -0
- package/template/components/ui/label.tsx +24 -0
- package/template/components/ui/pagination.tsx +117 -0
- package/template/components/ui/select.tsx +92 -0
- package/template/components/ui/sheet.tsx +135 -0
- package/template/components/ui/skeleton.tsx +15 -0
- package/template/components/ui/table.tsx +120 -0
- package/template/components/ui/tabs.tsx +55 -0
- package/template/lib/utils.ts +35 -0
- package/template/next.config.mjs +52 -0
- package/template/package.json +46 -0
- package/template/postcss.config.js +6 -0
- package/template/scripts/start-shopping-backend.sh +56 -0
- package/template/tailwind.config.ts +96 -0
- package/template/tsconfig.json +29 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { OpenApiConverter } from "@typia/utils";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { toEndpoints, unsupportedOperations } from "./toEndpoints";
|
|
5
|
+
|
|
6
|
+
// Build a normalized OpenApi.IDocument from a raw paths/schemas fragment, the
|
|
7
|
+
// same way fromSwagger does (upgradeDocument). Keeps the fixtures terse.
|
|
8
|
+
function doc(
|
|
9
|
+
paths: Record<string, unknown>,
|
|
10
|
+
schemas: Record<string, unknown> = {},
|
|
11
|
+
) {
|
|
12
|
+
return OpenApiConverter.upgradeDocument({
|
|
13
|
+
openapi: "3.0.0",
|
|
14
|
+
info: { title: "t", version: "1.0.0" },
|
|
15
|
+
paths,
|
|
16
|
+
components: { schemas },
|
|
17
|
+
// biome-ignore lint: test fixture
|
|
18
|
+
} as never);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const okJson = (ref: string) => ({
|
|
22
|
+
200: {
|
|
23
|
+
description: "ok",
|
|
24
|
+
content: { "application/json": { schema: { $ref: ref } } },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("toEndpoints — swagger-native READ layer", () => {
|
|
29
|
+
it("preserves query-string endpoints (the bug swagger-native fixed)", () => {
|
|
30
|
+
const eps = toEndpoints(
|
|
31
|
+
doc(
|
|
32
|
+
{
|
|
33
|
+
"/items/search": {
|
|
34
|
+
get: {
|
|
35
|
+
operationId: "search",
|
|
36
|
+
parameters: [
|
|
37
|
+
{ name: "q", in: "query", required: true, schema: { type: "string" } },
|
|
38
|
+
{ name: "page", in: "query", schema: { type: "integer" } },
|
|
39
|
+
],
|
|
40
|
+
responses: okJson("#/components/schemas/Item"),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{ Item: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
const search = eps.find((e) => e.path === "/items/search");
|
|
48
|
+
expect(search).toBeDefined();
|
|
49
|
+
expect(search!.query).not.toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("builds the query as a resolvable inline object (not nestia's dead $ref)", () => {
|
|
53
|
+
// nestia points route.query.schema at a synthetic `*.GetQuery` $ref that
|
|
54
|
+
// exists in no components map — reconstruct it inline from the parameters so
|
|
55
|
+
// a list call can fill required defaults instead of sending `query: {}`.
|
|
56
|
+
const eps = toEndpoints(
|
|
57
|
+
doc(
|
|
58
|
+
{
|
|
59
|
+
"/pet/findByStatus": {
|
|
60
|
+
get: {
|
|
61
|
+
operationId: "findByStatus",
|
|
62
|
+
parameters: [
|
|
63
|
+
{ name: "status", in: "query", required: true, schema: { type: "string", enum: ["available", "pending", "sold"] } },
|
|
64
|
+
{ name: "limit", in: "query", schema: { type: "integer" } },
|
|
65
|
+
],
|
|
66
|
+
responses: okJson("#/components/schemas/Pet"),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{ Pet: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
const q = eps.find((e) => e.path === "/pet/findByStatus")!.query as {
|
|
74
|
+
type: string;
|
|
75
|
+
properties: Record<string, unknown>;
|
|
76
|
+
required: string[];
|
|
77
|
+
};
|
|
78
|
+
expect(q.type).toBe("object");
|
|
79
|
+
expect(Object.keys(q.properties).sort()).toEqual(["limit", "status"]);
|
|
80
|
+
expect(q.required).toEqual(["status"]); // required flag preserved
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("promotes inline request/response bodies into named component schemas", () => {
|
|
84
|
+
const d = doc({
|
|
85
|
+
"/items": {
|
|
86
|
+
post: {
|
|
87
|
+
operationId: "create",
|
|
88
|
+
requestBody: {
|
|
89
|
+
content: {
|
|
90
|
+
"application/json": {
|
|
91
|
+
schema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: { name: { type: "string" } },
|
|
94
|
+
required: ["name"],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
responses: {
|
|
100
|
+
201: {
|
|
101
|
+
description: "created",
|
|
102
|
+
content: {
|
|
103
|
+
"application/json": {
|
|
104
|
+
schema: { type: "object", properties: { id: { type: "string" } } },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const ep = toEndpoints(d).find((e) => e.path === "/items")!;
|
|
113
|
+
// Inline body is not dropped — it is promoted to a named component and the
|
|
114
|
+
// promoted name resolves inside the document's components.
|
|
115
|
+
expect(ep.requestBody).not.toBeNull();
|
|
116
|
+
expect((d.components?.schemas ?? {})[ep.requestBody!.typeName]).toBeDefined();
|
|
117
|
+
expect(ep.responseBody).not.toBeNull();
|
|
118
|
+
expect((d.components?.schemas ?? {})[ep.responseBody!.typeName]).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("picks a 202 success body, never the error `default` response", () => {
|
|
122
|
+
// nestia only recognizes 200/201 as success; with a 202-only operation it
|
|
123
|
+
// falls back to `default` (the error schema). The real success is the 202.
|
|
124
|
+
const d = doc(
|
|
125
|
+
{
|
|
126
|
+
"/zip_downloads": {
|
|
127
|
+
post: {
|
|
128
|
+
operationId: "create",
|
|
129
|
+
responses: {
|
|
130
|
+
202: { description: "accepted", content: { "application/json": { schema: { $ref: "#/components/schemas/ZipDownload" } } } },
|
|
131
|
+
400: { description: "bad", content: { "application/json": { schema: { $ref: "#/components/schemas/ClientError" } } } },
|
|
132
|
+
default: { description: "err", content: { "application/json": { schema: { $ref: "#/components/schemas/ClientError" } } } },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
ZipDownload: { type: "object", properties: { id: { type: "string" }, download_url: { type: "string" } }, required: ["id"] },
|
|
139
|
+
ClientError: { type: "object", properties: { code: { type: "string" }, message: { type: "string" } } },
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
const ep = toEndpoints(d).find((e) => e.path === "/zip_downloads")!;
|
|
143
|
+
expect(ep.responseBody?.typeName).toBe("ZipDownload");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("treats a 204 no-content response as a null body, not the error `default`", () => {
|
|
147
|
+
const d = doc(
|
|
148
|
+
{
|
|
149
|
+
"/files/{id}": {
|
|
150
|
+
delete: {
|
|
151
|
+
operationId: "erase",
|
|
152
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
153
|
+
responses: {
|
|
154
|
+
204: { description: "deleted" },
|
|
155
|
+
default: { description: "err", content: { "application/json": { schema: { $ref: "#/components/schemas/ClientError" } } } },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{ ClientError: { type: "object", properties: { code: { type: "string" } } } },
|
|
161
|
+
);
|
|
162
|
+
const ep = toEndpoints(d).find((e) => e.path === "/files/{id}")!;
|
|
163
|
+
expect(ep.responseBody).toBeNull();
|
|
164
|
+
expect(ep.responseSchema).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("surfaces operations dropped for unsupported content types instead of losing them", () => {
|
|
168
|
+
const d = doc({
|
|
169
|
+
"/files/{id}": {
|
|
170
|
+
post: {
|
|
171
|
+
operationId: "upload",
|
|
172
|
+
requestBody: {
|
|
173
|
+
content: {
|
|
174
|
+
"application/octet-stream": {
|
|
175
|
+
schema: { type: "string", format: "binary" },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
180
|
+
responses: { 200: { description: "ok" } },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
const eps = toEndpoints(d);
|
|
185
|
+
const dropped = unsupportedOperations(d);
|
|
186
|
+
// The octet-stream op cannot become a route, but it must NOT vanish silently.
|
|
187
|
+
expect(eps.find((e) => e.path === "/files/{id}")).toBeUndefined();
|
|
188
|
+
expect(dropped.some((u) => u.path === "/files/{id}")).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("exposes a list endpoint with MORE THAN ONE object query param (the dropped-list bug)", () => {
|
|
192
|
+
// nestia's HttpMigration refuses a route whose query has >1 object-typed
|
|
193
|
+
// parameter ("query typed parameters must be only one object type"). Apideck's
|
|
194
|
+
// `GET /accounting/invoices` carries `filter`, `sort` AND `pass_through`
|
|
195
|
+
// objects — the very list screen that produces ids for navigation. It must be
|
|
196
|
+
// exposed, and its reconstructed query must keep EVERY param (objects + prims).
|
|
197
|
+
const d = doc(
|
|
198
|
+
{
|
|
199
|
+
"/accounting/invoices": {
|
|
200
|
+
get: {
|
|
201
|
+
operationId: "invoicesAll",
|
|
202
|
+
parameters: [
|
|
203
|
+
{ name: "filter", in: "query", schema: { $ref: "#/components/schemas/InvoicesFilter" } },
|
|
204
|
+
{ name: "sort", in: "query", schema: { $ref: "#/components/schemas/InvoicesSort" } },
|
|
205
|
+
{ name: "pass_through", in: "query", schema: { type: "object", properties: { k: { type: "string" } } } },
|
|
206
|
+
{ name: "limit", in: "query", schema: { type: "integer" } },
|
|
207
|
+
],
|
|
208
|
+
responses: okJson("#/components/schemas/Invoice"),
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
Invoice: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
214
|
+
InvoicesFilter: { type: "object", properties: { number: { type: "string" } } },
|
|
215
|
+
InvoicesSort: { type: "object", properties: { by: { type: "string" } } },
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
const ep = toEndpoints(d).find((e) => e.path === "/accounting/invoices");
|
|
219
|
+
expect(ep).toBeDefined();
|
|
220
|
+
// No longer reported as an unexposed/dropped operation.
|
|
221
|
+
expect(unsupportedOperations(d).some((u) => u.path === "/accounting/invoices")).toBe(false);
|
|
222
|
+
// The full query is preserved — both object params AND the primitive.
|
|
223
|
+
const q = ep!.query as { type: string; properties: Record<string, unknown> };
|
|
224
|
+
expect(q).not.toBeNull();
|
|
225
|
+
expect(Object.keys(q.properties).sort()).toEqual(["filter", "limit", "pass_through", "sort"]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("captures accessor, method, and path for every supported operation", () => {
|
|
229
|
+
const eps = toEndpoints(
|
|
230
|
+
doc(
|
|
231
|
+
{
|
|
232
|
+
"/items": { get: { operationId: "list", responses: okJson("#/components/schemas/Item") } },
|
|
233
|
+
"/items/{id}": {
|
|
234
|
+
get: {
|
|
235
|
+
operationId: "get",
|
|
236
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
237
|
+
responses: okJson("#/components/schemas/Item"),
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{ Item: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
expect(eps).toHaveLength(2);
|
|
245
|
+
for (const ep of eps) {
|
|
246
|
+
expect(ep.method).toBeTruthy();
|
|
247
|
+
expect(ep.path).toBeTruthy();
|
|
248
|
+
expect(ep.accessor.length).toBeGreaterThan(0);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { OpenApi } from "@typia/interface";
|
|
2
|
+
import { HttpMigration, OpenApiTypeChecker } from "@typia/utils";
|
|
3
|
+
|
|
4
|
+
import { normalizeForNestia } from "./normalizeForNestia";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Swagger-native endpoint model — Step toward "swagger만 읽고 정확하게".
|
|
8
|
+
*
|
|
9
|
+
* Built directly from nestia's `HttpMigration.application(doc).routes`, the
|
|
10
|
+
* single source of truth that already carries accessor + query + path params +
|
|
11
|
+
* body + response for EVERY operation. Replaces the former
|
|
12
|
+
* `invertOpenApiDocument`, whose `r.query === null` filter silently dropped
|
|
13
|
+
* every query-string endpoint (search / filter / pagination) — see the bug
|
|
14
|
+
* this migration fixes.
|
|
15
|
+
*
|
|
16
|
+
* Shape mirrors the former endpoint model so the orchestrators change minimally
|
|
17
|
+
* (`document.operations` → `toEndpoints(document)`), plus the one field the old
|
|
18
|
+
* model could not express: {@link IAutoViewEndpoint.query}.
|
|
19
|
+
*
|
|
20
|
+
* Schemas are typed as `OpenApi.IJsonSchema` purely for structural reuse
|
|
21
|
+
* — at runtime they ARE standard OpenAPI schemas (the old invert did the same
|
|
22
|
+
* `as any` cast), so the existing `describeEndpoint*` / `renderJsonSchema`
|
|
23
|
+
* helpers keep working unchanged.
|
|
24
|
+
*/
|
|
25
|
+
export interface IAutoViewEndpoint {
|
|
26
|
+
method: string;
|
|
27
|
+
path: string;
|
|
28
|
+
|
|
29
|
+
/** SDK function path nestia assigns, e.g. `["items","search","get"]`. */
|
|
30
|
+
accessor: string[];
|
|
31
|
+
|
|
32
|
+
/** Last accessor segment — the SDK function name. */
|
|
33
|
+
name: string;
|
|
34
|
+
|
|
35
|
+
/** Human description (summary + description merged). */
|
|
36
|
+
description: string;
|
|
37
|
+
|
|
38
|
+
/** Path parameters (primitive). */
|
|
39
|
+
parameters: IAutoViewEndpoint.IParameter[];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Query-string schema as a single object, or `null` when the endpoint has no
|
|
43
|
+
* query. THIS is what the old AutoBE model could not hold.
|
|
44
|
+
*/
|
|
45
|
+
query: OpenApi.IJsonSchema | null;
|
|
46
|
+
|
|
47
|
+
/** Request body reference, or `null`. */
|
|
48
|
+
requestBody: IAutoViewEndpoint.IBody | null;
|
|
49
|
+
|
|
50
|
+
/** Response body reference, or `null`. */
|
|
51
|
+
responseBody: IAutoViewEndpoint.IBody | null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The raw success response schema, captured even when {@link responseBody} is
|
|
55
|
+
* `null` because the response is an INLINE object (not a named `$ref`) — e.g.
|
|
56
|
+
* DigitalOcean's `{ databases: [...] }`. Lets the consumability analysis
|
|
57
|
+
* introspect inline collection responses without changing the frontend path
|
|
58
|
+
* (which keys off the named {@link responseBody.typeName}).
|
|
59
|
+
*/
|
|
60
|
+
responseSchema: OpenApi.IJsonSchema | null;
|
|
61
|
+
}
|
|
62
|
+
export namespace IAutoViewEndpoint {
|
|
63
|
+
export interface IParameter {
|
|
64
|
+
name: string;
|
|
65
|
+
description: string;
|
|
66
|
+
schema: OpenApi.IJsonSchema;
|
|
67
|
+
}
|
|
68
|
+
export interface IBody {
|
|
69
|
+
description: string;
|
|
70
|
+
/**
|
|
71
|
+
* Component schema name. For an array response (`Pet[]`) this is the
|
|
72
|
+
* ELEMENT type (`Pet`) with {@link isArray} set — so a list screen knows
|
|
73
|
+
* its row type and can derive table columns deterministically.
|
|
74
|
+
*/
|
|
75
|
+
typeName: string;
|
|
76
|
+
/** True when the payload is an array of `typeName` (list response). */
|
|
77
|
+
isArray: boolean;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Convert a standard OpenAPI document into AutoView's endpoint list — every
|
|
83
|
+
* operation, query endpoints included.
|
|
84
|
+
*/
|
|
85
|
+
/** A segment usable as a JS property in `api.functional.<seg>`. */
|
|
86
|
+
const JS_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* True when every accessor segment is a valid JS identifier, so the generator
|
|
90
|
+
* can emit `api.functional.<a>.<b>(…)`. Some real swaggers (Box) use `#`
|
|
91
|
+
* fragments in their paths (`/shared_items#folders`, `/schema#add`), which
|
|
92
|
+
* nestia turns into accessor segments containing `#` — unusable as a dotted
|
|
93
|
+
* property and a hard syntax error in the generated TSX. Such operations are
|
|
94
|
+
* dropped from {@link toEndpoints} and surfaced by {@link unsupportedOperations}.
|
|
95
|
+
*/
|
|
96
|
+
export function isRenderableAccessor(accessor: string[]): boolean {
|
|
97
|
+
return accessor.length > 0 && accessor.every((s) => JS_IDENT.test(s));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function toEndpoints(document: OpenApi.IDocument): IAutoViewEndpoint[] {
|
|
101
|
+
const app = HttpMigration.application(normalizeForNestia(document));
|
|
102
|
+
return app.routes
|
|
103
|
+
.filter((r) => isRenderableAccessor(r.accessor))
|
|
104
|
+
.map((r): IAutoViewEndpoint => {
|
|
105
|
+
const body = resolveBody(r.body);
|
|
106
|
+
const successPart = pickSuccess(r);
|
|
107
|
+
const success = resolveBody(successPart);
|
|
108
|
+
// Read description and query from the UNTOUCHED original operation, not the
|
|
109
|
+
// relaxed one HttpMigration saw — the relaxed copy may have shed some object
|
|
110
|
+
// query params just to let the route compose (see normalizeForNestia).
|
|
111
|
+
const operation = originalOperation(document, r.method, r.path) ?? r.operation();
|
|
112
|
+
return {
|
|
113
|
+
method: r.method,
|
|
114
|
+
path: r.path,
|
|
115
|
+
accessor: r.accessor,
|
|
116
|
+
name: r.accessor.at(-1) ?? r.path,
|
|
117
|
+
description: writeDescription(operation) ?? "",
|
|
118
|
+
parameters: r.parameters.map((p) => ({
|
|
119
|
+
name: p.key,
|
|
120
|
+
description: p.parameter().description ?? "",
|
|
121
|
+
// biome-ignore lint: structurally an OpenAPI schema, reused as-is
|
|
122
|
+
schema: p.schema as unknown as OpenApi.IJsonSchema,
|
|
123
|
+
})),
|
|
124
|
+
query: buildQuerySchema(operation, document),
|
|
125
|
+
requestBody: body,
|
|
126
|
+
responseBody: success,
|
|
127
|
+
responseSchema: successPart
|
|
128
|
+
? // biome-ignore lint: structurally an OpenAPI schema, reused as-is
|
|
129
|
+
(successPart.schema as unknown as OpenApi.IJsonSchema)
|
|
130
|
+
: null,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Look up the source operation in the original document by method + path. */
|
|
136
|
+
function originalOperation(
|
|
137
|
+
document: OpenApi.IDocument,
|
|
138
|
+
method: string,
|
|
139
|
+
path: string,
|
|
140
|
+
): OpenApi.IOperation | undefined {
|
|
141
|
+
const item = (document.paths ?? {})[path] as
|
|
142
|
+
| Record<string, OpenApi.IOperation>
|
|
143
|
+
| undefined;
|
|
144
|
+
return item?.[method];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Build the query-object schema from the raw operation's `in: query`
|
|
149
|
+
* parameters, as an INLINE object (`{ type, properties, required }`).
|
|
150
|
+
*
|
|
151
|
+
* nestia's `route.query.schema` is a `$ref` to a synthetic `*.GetQuery` type
|
|
152
|
+
* that is registered in NEITHER the original document NOR the migration's own
|
|
153
|
+
* document — a dead reference. Resolving it yields nothing, so a list call's
|
|
154
|
+
* defaults came out empty (`query: {}`) and the server 400'd on a required
|
|
155
|
+
* param (petstore's `status`). Reconstructing the schema from the source
|
|
156
|
+
* parameters keeps it inline and resolvable by every consumer, and preserves
|
|
157
|
+
* each param's `required` flag, `enum`, and `default`.
|
|
158
|
+
*/
|
|
159
|
+
function buildQuerySchema(
|
|
160
|
+
operation: OpenApi.IOperation | undefined,
|
|
161
|
+
document: OpenApi.IDocument,
|
|
162
|
+
): OpenApi.IJsonSchema | null {
|
|
163
|
+
const raw = operation?.parameters ?? [];
|
|
164
|
+
const properties: Record<string, OpenApi.IJsonSchema> = {};
|
|
165
|
+
const required: string[] = [];
|
|
166
|
+
for (const entry of raw) {
|
|
167
|
+
const p = derefParameter(entry, document);
|
|
168
|
+
if (p === undefined || p.in !== "query") continue;
|
|
169
|
+
properties[p.name] = (p.schema as OpenApi.IJsonSchema | undefined) ?? {};
|
|
170
|
+
if (p.required === true) required.push(p.name);
|
|
171
|
+
}
|
|
172
|
+
if (Object.keys(properties).length === 0) return null;
|
|
173
|
+
// biome-ignore lint: structurally an OpenAPI object schema
|
|
174
|
+
return { type: "object", properties, required } as OpenApi.IJsonSchema;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface IRawParameter {
|
|
178
|
+
name: string;
|
|
179
|
+
in: string;
|
|
180
|
+
required?: boolean;
|
|
181
|
+
schema?: unknown;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Resolve a `$ref` parameter against `components.parameters`; pass others through. */
|
|
185
|
+
function derefParameter(
|
|
186
|
+
entry: unknown,
|
|
187
|
+
document: OpenApi.IDocument,
|
|
188
|
+
): IRawParameter | undefined {
|
|
189
|
+
if (entry !== null && typeof entry === "object" && "$ref" in entry) {
|
|
190
|
+
const ref = (entry as { $ref: unknown }).$ref;
|
|
191
|
+
if (typeof ref !== "string") return undefined;
|
|
192
|
+
const name = ref.split("/").pop();
|
|
193
|
+
const params = (document.components as { parameters?: Record<string, unknown> } | undefined)
|
|
194
|
+
?.parameters;
|
|
195
|
+
const target = name !== undefined ? params?.[name] : undefined;
|
|
196
|
+
return target as IRawParameter | undefined;
|
|
197
|
+
}
|
|
198
|
+
return entry as IRawParameter | undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 2xx status codes whose JSON body is the success payload, in preference order. */
|
|
202
|
+
const SUCCESS_CODES = ["200", "201", "202", "203", "206"] as const;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Pick the real success-response body for a route.
|
|
206
|
+
*
|
|
207
|
+
* nestia's `HttpMigration` only recognizes `200`/`201` as success; when an
|
|
208
|
+
* operation's only 2xx is `202 Accepted` (a body that exists — e.g. Box's
|
|
209
|
+
* `POST /zip_downloads` → `ZipDownload`) or `204 No Content`, it falls back to
|
|
210
|
+
* the `default` response, which is almost always the ERROR schema
|
|
211
|
+
* (`ClientError`). Trusting that makes a screen render error fields (`code`,
|
|
212
|
+
* `message`, `request_id`) as columns and breaks producer tracing.
|
|
213
|
+
*
|
|
214
|
+
* So we trust `route.success` only when its status is genuinely 2xx; otherwise
|
|
215
|
+
* we re-pick from the raw operation's 2xx responses ourselves. A `204` (or any
|
|
216
|
+
* 2xx with no JSON body) correctly yields `null` — no body, not an error body.
|
|
217
|
+
*
|
|
218
|
+
* Trusting nestia when it IS 2xx matters: nestia promotes an inline success body
|
|
219
|
+
* into a named component (`IApiItems.PostResponse`), which the re-pick path (raw
|
|
220
|
+
* inline object, no `$ref`) cannot reproduce. The status it reports is a STRING
|
|
221
|
+
* (`"201"`, `"default"`), so we parse it numerically.
|
|
222
|
+
*/
|
|
223
|
+
function pickSuccess(route: {
|
|
224
|
+
success?: IBodyPart | null;
|
|
225
|
+
operation: () => OpenApi.IOperation | undefined;
|
|
226
|
+
}): IBodyPart | null {
|
|
227
|
+
const status = Number(
|
|
228
|
+
(route.success as { status?: unknown } | null | undefined)?.status,
|
|
229
|
+
);
|
|
230
|
+
if (Number.isInteger(status) && status >= 200 && status < 300) {
|
|
231
|
+
return route.success ?? null;
|
|
232
|
+
}
|
|
233
|
+
const responses = route.operation()?.responses ?? {};
|
|
234
|
+
for (const code of SUCCESS_CODES) {
|
|
235
|
+
const response = responses[code] as
|
|
236
|
+
| { description?: string; content?: Record<string, { schema?: unknown }> }
|
|
237
|
+
| undefined;
|
|
238
|
+
const schema = response?.content?.["application/json"]?.schema;
|
|
239
|
+
if (schema !== undefined) {
|
|
240
|
+
return {
|
|
241
|
+
type: "application/json",
|
|
242
|
+
schema: schema as OpenApi.IJsonSchema,
|
|
243
|
+
description: () => response?.description,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return null; // 204 / no JSON body — a genuine empty response, not an error
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Operations `HttpMigration` could not turn into a route at all — typically an
|
|
252
|
+
* unsupported request/response content type (e.g. `application/octet-stream`,
|
|
253
|
+
* raw binary upload/download). These never reach {@link toEndpoints}, so
|
|
254
|
+
* surfacing them is the only way to keep the READ layer from dropping
|
|
255
|
+
* endpoints silently.
|
|
256
|
+
*/
|
|
257
|
+
export interface IUnsupportedOperation {
|
|
258
|
+
method: string;
|
|
259
|
+
path: string;
|
|
260
|
+
reason: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function unsupportedOperations(
|
|
264
|
+
document: OpenApi.IDocument,
|
|
265
|
+
): IUnsupportedOperation[] {
|
|
266
|
+
// Same relaxation as toEndpoints, so multi-object-query routes that now
|
|
267
|
+
// compose are not double-counted here as "dropped".
|
|
268
|
+
const app = HttpMigration.application(normalizeForNestia(document));
|
|
269
|
+
const fromErrors = (app.errors ?? []).map((e): IUnsupportedOperation => {
|
|
270
|
+
const messages: string[] = Array.isArray(e.messages) ? e.messages : [];
|
|
271
|
+
return {
|
|
272
|
+
method: (e.method ?? "?").toString().toUpperCase(),
|
|
273
|
+
path: e.path ?? "?",
|
|
274
|
+
reason: messages.join("; ") || "unsupported operation",
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
// Routes whose accessor is not a valid JS identifier chain (path fragments
|
|
278
|
+
// like `/shared_items#folders`) are dropped by `toEndpoints` — report them so
|
|
279
|
+
// nothing vanishes silently.
|
|
280
|
+
const fromAccessors = app.routes
|
|
281
|
+
.filter((r) => !isRenderableAccessor(r.accessor))
|
|
282
|
+
.map((r): IUnsupportedOperation => ({
|
|
283
|
+
method: r.method.toUpperCase(),
|
|
284
|
+
path: r.path,
|
|
285
|
+
reason: `Accessor contains a non-identifier segment (${r.accessor.join(".")}) — cannot generate a typed SDK call.`,
|
|
286
|
+
}));
|
|
287
|
+
return [...fromErrors, ...fromAccessors];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface IBodyPart {
|
|
291
|
+
type: string;
|
|
292
|
+
schema: OpenApi.IJsonSchema;
|
|
293
|
+
description: () => string | undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Resolve a request/response body part into the element type + array flag.
|
|
298
|
+
* Handles both a named reference (`{ $ref: Pet }` → `Pet`, single) and an
|
|
299
|
+
* array of references (`{ type: array, items: { $ref: Pet } }` → `Pet`,
|
|
300
|
+
* isArray) — the latter is the list-response shape the old code dropped,
|
|
301
|
+
* leaving table screens with no deterministic column source.
|
|
302
|
+
*/
|
|
303
|
+
function resolveBody(
|
|
304
|
+
part: IBodyPart | null | undefined,
|
|
305
|
+
): IAutoViewEndpoint.IBody | null {
|
|
306
|
+
if (!part || part.type !== "application/json") return null;
|
|
307
|
+
const schema = part.schema;
|
|
308
|
+
if (OpenApiTypeChecker.isReference(schema)) {
|
|
309
|
+
return {
|
|
310
|
+
description: part.description() ?? "",
|
|
311
|
+
typeName: refName(schema.$ref),
|
|
312
|
+
isArray: false,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (OpenApiTypeChecker.isArray(schema)) {
|
|
316
|
+
const items = schema.items;
|
|
317
|
+
if (OpenApiTypeChecker.isReference(items)) {
|
|
318
|
+
return {
|
|
319
|
+
description: part.description() ?? "",
|
|
320
|
+
typeName: refName(items.$ref),
|
|
321
|
+
isArray: true,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function refName($ref: string): string {
|
|
329
|
+
return $ref.split("/").pop()!;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function writeDescription(
|
|
333
|
+
operation: OpenApi.IOperation | undefined,
|
|
334
|
+
): string | undefined {
|
|
335
|
+
if (operation === undefined) return undefined;
|
|
336
|
+
if (operation.summary === undefined && operation.description === undefined)
|
|
337
|
+
return undefined;
|
|
338
|
+
if (operation.summary === undefined) return operation.description;
|
|
339
|
+
if (operation.description === undefined) return operation.summary;
|
|
340
|
+
if (operation.description.startsWith(operation.summary))
|
|
341
|
+
return operation.description;
|
|
342
|
+
return `${operation.summary}${operation.summary.endsWith(".") ? "" : "."}\n\n${operation.description}`;
|
|
343
|
+
}
|