zudoku 0.0.0-fix-circular-ref-false-positives.zbe02c6a6 → 0.0.0-fix-warnings.z053d4e27
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/dist/config/validators/InputNavigationSchema.d.ts +56 -56
- package/dist/config/validators/ProtectedRoutesSchema.d.ts +1 -1
- package/dist/config/validators/validate.d.ts +1 -1
- package/dist/lib/components/Heading.d.ts +1 -1
- package/dist/lib/components/index.d.ts +18 -74
- package/dist/lib/components/index.js +19 -36
- package/dist/lib/components/index.js.map +1 -1
- package/dist/lib/hooks/index.d.ts +7 -30
- package/dist/lib/hooks/index.js +7 -15
- package/dist/lib/hooks/index.js.map +1 -1
- package/dist/lib/oas/graphql/circular.d.ts +1 -1
- package/dist/lib/oas/graphql/circular.js +35 -18
- package/dist/lib/oas/graphql/circular.js.map +1 -1
- package/dist/lib/oas/graphql/circular.test.js +2 -33
- package/dist/lib/oas/graphql/circular.test.js.map +1 -1
- package/dist/lib/oas/parser/index.js +14 -5
- package/dist/lib/oas/parser/index.js.map +1 -1
- package/dist/lib/util/flattenAllOf.d.ts +0 -2
- package/dist/lib/util/flattenAllOf.js +0 -46
- package/dist/lib/util/flattenAllOf.js.map +1 -1
- package/dist/lib/util/flattenAllOf.test.js +2 -1
- package/dist/lib/util/flattenAllOf.test.js.map +1 -1
- package/dist/lib/util/flattenAllOfProcessor.d.ts +2 -0
- package/dist/lib/util/flattenAllOfProcessor.js +48 -0
- package/dist/lib/util/flattenAllOfProcessor.js.map +1 -0
- package/dist/lib/util/readFrontmatter.js +2 -1
- package/dist/lib/util/readFrontmatter.js.map +1 -1
- package/dist/vite/api/SchemaManager.js +1 -1
- package/dist/vite/api/SchemaManager.js.map +1 -1
- package/dist/vite/api/SchemaManager.test.js +1 -1
- package/dist/vite/api/SchemaManager.test.js.map +1 -1
- package/dist/vite/build.js +91 -73
- package/dist/vite/build.js.map +1 -1
- package/dist/vite/mdx/remark-inject-filepath.js +5 -1
- package/dist/vite/mdx/remark-inject-filepath.js.map +1 -1
- package/dist/vite/mdx/remark-link-rewrite.js +3 -2
- package/dist/vite/mdx/remark-link-rewrite.js.map +1 -1
- package/dist/vite/plugin-docs.js +9 -7
- package/dist/vite/plugin-docs.js.map +1 -1
- package/dist/vite/plugin-markdown-export.js +4 -2
- package/dist/vite/plugin-markdown-export.js.map +1 -1
- package/lib/{ClaudeLogo-DJ9bU-sO.js → ClaudeLogo-C6q-Xn_l.js} +26 -22
- package/lib/{ClaudeLogo-DJ9bU-sO.js.map → ClaudeLogo-C6q-Xn_l.js.map} +1 -1
- package/lib/{MdxPage-stpAoBtx.js → MdxPage-B1G4W1TK.js} +8 -8
- package/lib/{MdxPage-stpAoBtx.js.map → MdxPage-B1G4W1TK.js.map} +1 -1
- package/lib/{Mermaid-Koc3z8mU.js → Mermaid-B1xNo-pf.js} +3 -2
- package/lib/{Mermaid-Koc3z8mU.js.map → Mermaid-B1xNo-pf.js.map} +1 -1
- package/lib/{OAuthErrorPage-DJ811Bn_.js → OAuthErrorPage-01Ke086W.js} +20 -18
- package/lib/{OAuthErrorPage-DJ811Bn_.js.map → OAuthErrorPage-01Ke086W.js.map} +1 -1
- package/lib/{OasProvider-B2KxIBsI.js → OasProvider-BG-FWDIq.js} +3 -3
- package/lib/{OasProvider-B2KxIBsI.js.map → OasProvider-BG-FWDIq.js.map} +1 -1
- package/lib/{OperationList-C2tAfThO.js → OperationList-GGkJ1vac.js} +948 -945
- package/lib/OperationList-GGkJ1vac.js.map +1 -0
- package/lib/{RouteGuard--A04ESy8.js → RouteGuard-B1lCR0C_.js} +5 -5
- package/lib/{RouteGuard--A04ESy8.js.map → RouteGuard-B1lCR0C_.js.map} +1 -1
- package/lib/{SchemaList-Ep8DleP_.js → SchemaList-CNVdC9f-.js} +7 -7
- package/lib/{SchemaList-Ep8DleP_.js.map → SchemaList-CNVdC9f-.js.map} +1 -1
- package/lib/{SchemaView-BpaEKRYx.js → SchemaView-CrV0yIwR.js} +3 -3
- package/lib/{SchemaView-BpaEKRYx.js.map → SchemaView-CrV0yIwR.js.map} +1 -1
- package/lib/{SignUp-DCBViNUi.js → SignUp-8kDBaLbO.js} +31 -26
- package/lib/{SignUp-DCBViNUi.js.map → SignUp-8kDBaLbO.js.map} +1 -1
- package/lib/{SyntaxHighlight-Dshjn3Zf.js → SyntaxHighlight-hZOFnYl0.js} +3 -3
- package/lib/{SyntaxHighlight-Dshjn3Zf.js.map → SyntaxHighlight-hZOFnYl0.js.map} +1 -1
- package/lib/{Toc-Cgz6CPiE.js → Toc-qEIii_-W.js} +2 -2
- package/lib/{Toc-Cgz6CPiE.js.map → Toc-qEIii_-W.js.map} +1 -1
- package/lib/{index-CL8eDnQW.js → Zudoku-DUsdmPME.js} +2250 -2268
- package/lib/Zudoku-DUsdmPME.js.map +1 -0
- package/lib/{ZudokuContext-BZB1TWdT.js → ZudokuContext-BBI06sOx.js} +5 -5
- package/lib/{ZudokuContext-BZB1TWdT.js.map → ZudokuContext-BBI06sOx.js.map} +1 -1
- package/lib/{circular-CG3e0_Uz.js → circular-bbWO95zs.js} +1346 -1327
- package/lib/{circular-CG3e0_Uz.js.map → circular-bbWO95zs.js.map} +1 -1
- package/lib/createServer-B7POuwZp.js +13036 -0
- package/lib/createServer-B7POuwZp.js.map +1 -0
- package/lib/{errors-b9I-fAOY.js → errors-7hgPDs1h.js} +3 -3
- package/lib/{errors-b9I-fAOY.js.map → errors-7hgPDs1h.js.map} +1 -1
- package/lib/{firebase-BCXX7Qv5.js → firebase-Dwn-2ju-.js} +13 -13
- package/lib/firebase-Dwn-2ju-.js.map +1 -0
- package/lib/{hook-BGlHBdET.js → hook-ZEd1Es7D.js} +2 -2
- package/lib/{hook-BGlHBdET.js.map → hook-ZEd1Es7D.js.map} +1 -1
- package/lib/{index-I3kmZ7tG.js → index-CTCT4jlW.js} +463 -461
- package/lib/{index-I3kmZ7tG.js.map → index-CTCT4jlW.js.map} +1 -1
- package/lib/index-DAWHN3cH.js +86 -0
- package/lib/index-DAWHN3cH.js.map +1 -0
- package/lib/{index-UOLtazB8.js → index-Dxdhrp-I.js} +2 -2
- package/lib/{index-UOLtazB8.js.map → index-Dxdhrp-I.js.map} +1 -1
- package/lib/{index.esm-B_0dvNjB.js → index.esm-Ca5zvoff.js} +20 -20
- package/lib/{index.esm-B_0dvNjB.js.map → index.esm-Ca5zvoff.js.map} +1 -1
- package/lib/{index.esm-C5CBsVzN.js → index.esm-DG4KaDKR.js} +2 -2
- package/lib/index.esm-DG4KaDKR.js.map +1 -0
- package/lib/{invariant-BJAl77rw.js → invariant-B_t_F2s_.js} +3 -3
- package/lib/{invariant-BJAl77rw.js.map → invariant-B_t_F2s_.js.map} +1 -1
- package/lib/ui/SyntaxHighlight.js +3 -3
- package/lib/useExposedProps-CzTDfXfq.js +30 -0
- package/lib/useExposedProps-CzTDfXfq.js.map +1 -0
- package/lib/zudoku.__internal.js +366 -365
- package/lib/zudoku.__internal.js.map +1 -1
- package/lib/zudoku.auth-auth0.js +1 -1
- package/lib/zudoku.auth-azureb2c.js +4 -4
- package/lib/zudoku.auth-clerk.js +2 -2
- package/lib/zudoku.auth-firebase.js +5 -5
- package/lib/zudoku.auth-openid.js +5 -5
- package/lib/zudoku.auth-supabase.js +4 -4
- package/lib/zudoku.components.js +31 -29
- package/lib/zudoku.components.js.map +1 -1
- package/lib/zudoku.hooks.js +24 -11
- package/lib/zudoku.hooks.js.map +1 -1
- package/lib/zudoku.mermaid.js +4 -3
- package/lib/zudoku.mermaid.js.map +1 -1
- package/lib/zudoku.plugin-api-catalog.js +36 -32
- package/lib/zudoku.plugin-api-catalog.js.map +1 -1
- package/lib/zudoku.plugin-api-keys.js +131 -130
- package/lib/zudoku.plugin-api-keys.js.map +1 -1
- package/lib/zudoku.plugin-custom-pages.js +1 -1
- package/lib/zudoku.plugin-markdown.js +1 -1
- package/lib/zudoku.plugin-openapi.js +2 -2
- package/lib/zudoku.plugin-search-pagefind.js +2 -2
- package/package.json +3 -2
- package/src/lib/components/index.ts +19 -39
- package/src/lib/hooks/index.ts +7 -16
- package/src/lib/oas/graphql/circular.test.ts +2 -37
- package/src/lib/oas/graphql/circular.ts +51 -25
- package/src/lib/oas/parser/index.ts +17 -6
- package/src/lib/util/flattenAllOf.test.ts +2 -1
- package/src/lib/util/flattenAllOf.ts +0 -57
- package/src/lib/util/flattenAllOfProcessor.ts +58 -0
- package/src/lib/util/readFrontmatter.ts +2 -1
- package/src/zuplo/enrich-with-zuplo-mcp.ts +168 -0
- package/src/zuplo/enrich-with-zuplo.ts +254 -0
- package/src/zuplo/policy-types.ts +46 -0
- package/src/zuplo/with-zuplo-processors.ts +35 -0
- package/src/zuplo/with-zuplo.ts +14 -0
- package/lib/OperationList-C2tAfThO.js.map +0 -1
- package/lib/___vite-browser-external_commonjs-proxy-BttVsNON.js +0 -9
- package/lib/___vite-browser-external_commonjs-proxy-BttVsNON.js.map +0 -1
- package/lib/createServer-CNeRqj98.js +0 -16693
- package/lib/createServer-CNeRqj98.js.map +0 -1
- package/lib/firebase-BCXX7Qv5.js.map +0 -1
- package/lib/index-CL8eDnQW.js.map +0 -1
- package/lib/index-DBjOT2H1.js +0 -133
- package/lib/index-DBjOT2H1.js.map +0 -1
- package/lib/index.esm-C5CBsVzN.js.map +0 -1
|
@@ -156,20 +156,16 @@ describe("handleCircularRefs", () => {
|
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
-
it("should
|
|
159
|
+
it("should deduplicate shared object instances with __$ref", () => {
|
|
160
160
|
const shared = { __$ref: "#/components/schemas/Foo", type: "string" };
|
|
161
161
|
const obj = { a: shared, b: shared };
|
|
162
162
|
const result = handleCircularRefs(obj);
|
|
163
163
|
|
|
164
|
-
// Both should return the cached result, not mark as circular
|
|
165
164
|
expect(result.a).toEqual({
|
|
166
165
|
__$ref: "#/components/schemas/Foo",
|
|
167
166
|
type: "string",
|
|
168
167
|
});
|
|
169
|
-
expect(result.b).
|
|
170
|
-
__$ref: "#/components/schemas/Foo",
|
|
171
|
-
type: "string",
|
|
172
|
-
});
|
|
168
|
+
expect(result.b).toBe(`${SCHEMA_REF_PREFIX}#/components/schemas/Foo`);
|
|
173
169
|
});
|
|
174
170
|
|
|
175
171
|
it("should mark circular ref with property name from path", () => {
|
|
@@ -187,35 +183,4 @@ describe("handleCircularRefs", () => {
|
|
|
187
183
|
|
|
188
184
|
expect(result.properties.child.properties.back).toContain(CIRCULAR_REF);
|
|
189
185
|
});
|
|
190
|
-
|
|
191
|
-
// Exact reproduction of #1869 - shared object instances with __$ref
|
|
192
|
-
it("should NOT mark shared object instances with __$ref as circular (issue #1869)", () => {
|
|
193
|
-
// When dereferencing, the SAME object instance is returned for all refs to the same schema
|
|
194
|
-
const timestampSchema = {
|
|
195
|
-
__$ref: "#/components/schemas/timestamp",
|
|
196
|
-
type: "string",
|
|
197
|
-
format: "date-time",
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
// Both created_at and updated_at point to the SAME object instance
|
|
201
|
-
const obj = {
|
|
202
|
-
type: "object",
|
|
203
|
-
properties: {
|
|
204
|
-
created_at: timestampSchema,
|
|
205
|
-
updated_at: timestampSchema,
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
const result = handleCircularRefs(obj);
|
|
210
|
-
|
|
211
|
-
// The first one should be fully expanded
|
|
212
|
-
expect(result.properties.created_at).toEqual({
|
|
213
|
-
__$ref: "#/components/schemas/timestamp",
|
|
214
|
-
type: "string",
|
|
215
|
-
format: "date-time",
|
|
216
|
-
});
|
|
217
|
-
// The second one should ALSO be fully expanded (not marked as circular)
|
|
218
|
-
expect(typeof result.properties.updated_at).toBe("object");
|
|
219
|
-
expect(result.properties.updated_at).not.toContain(CIRCULAR_REF);
|
|
220
|
-
});
|
|
221
186
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GraphQLScalarType } from "graphql/index.js";
|
|
2
2
|
import { GraphQLJSON } from "graphql-type-json";
|
|
3
|
+
import type { RecordAny } from "../../util/traverse.js";
|
|
3
4
|
|
|
4
5
|
export const CIRCULAR_REF = "$[Circular Reference]";
|
|
5
6
|
export const SCHEMA_REF_PREFIX = "$ref:";
|
|
@@ -16,48 +17,73 @@ const OPENAPI_PROPS = new Set([
|
|
|
16
17
|
export const handleCircularRefs = (
|
|
17
18
|
// biome-ignore lint/suspicious/noExplicitAny: Allow any type
|
|
18
19
|
obj: any,
|
|
19
|
-
|
|
20
|
+
visited = new WeakSet(),
|
|
20
21
|
refs = new WeakMap(),
|
|
21
22
|
path: string[] = [],
|
|
22
|
-
|
|
23
|
+
seenRefPaths = new Set<string>(),
|
|
23
24
|
// biome-ignore lint/suspicious/noExplicitAny: Allow any type
|
|
24
25
|
): any => {
|
|
25
26
|
if (obj === null || typeof obj !== "object") return obj;
|
|
26
27
|
|
|
27
28
|
const refPath = obj.__$ref;
|
|
28
|
-
const isCircular =
|
|
29
|
-
currentPath.has(obj) ||
|
|
30
|
-
(typeof refPath === "string" && currentRefPaths.has(refPath));
|
|
31
29
|
|
|
32
|
-
if (
|
|
33
|
-
|
|
30
|
+
// Check if this object has a __$ref marker (set during schema code generation)
|
|
31
|
+
// If we've already fully processed this ref path, return a reference marker
|
|
32
|
+
// instead of the full data to avoid JSON.stringify serializing duplicates
|
|
33
|
+
if (typeof refPath === "string" && seenRefPaths.has(refPath)) {
|
|
34
|
+
return SCHEMA_REF_PREFIX + refPath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (visited.has(obj)) {
|
|
38
|
+
const cached = refs.get(obj);
|
|
39
|
+
if (cached) {
|
|
40
|
+
return typeof refPath === "string"
|
|
41
|
+
? // If already processed, return ref marker to avoid duplicate serialization
|
|
42
|
+
SCHEMA_REF_PREFIX + refPath
|
|
43
|
+
: cached;
|
|
44
|
+
}
|
|
34
45
|
const circularProp = path.find((p) => !OPENAPI_PROPS.has(p)) || path[0];
|
|
46
|
+
|
|
35
47
|
return [CIRCULAR_REF, circularProp].filter(Boolean).join(":");
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
|
|
50
|
+
visited.add(obj);
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
// Add refPath BEFORE recursing to detect cycles within this branch
|
|
53
|
+
// This will be removed after processing to allow siblings with the same ref
|
|
54
|
+
if (typeof refPath === "string") {
|
|
55
|
+
seenRefPaths.add(refPath);
|
|
56
|
+
}
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
let result: RecordAny | RecordAny[];
|
|
59
|
+
if (Array.isArray(obj)) {
|
|
60
|
+
result = obj.map((item, index) =>
|
|
61
|
+
handleCircularRefs(
|
|
62
|
+
item,
|
|
63
|
+
visited,
|
|
64
|
+
refs,
|
|
65
|
+
[...path, index.toString()],
|
|
66
|
+
seenRefPaths,
|
|
67
|
+
),
|
|
50
68
|
);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
} else {
|
|
70
|
+
result = {};
|
|
71
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
72
|
+
result[key] = handleCircularRefs(
|
|
73
|
+
value,
|
|
74
|
+
visited,
|
|
75
|
+
refs,
|
|
76
|
+
[...path, key],
|
|
77
|
+
seenRefPaths,
|
|
56
78
|
);
|
|
57
|
-
|
|
79
|
+
}
|
|
80
|
+
}
|
|
58
81
|
refs.set(obj, result);
|
|
59
|
-
|
|
60
|
-
|
|
82
|
+
|
|
83
|
+
// Remove refPath after processing so sibling refs aren't incorrectly marked
|
|
84
|
+
if (typeof refPath === "string") {
|
|
85
|
+
seenRefPaths.delete(refPath);
|
|
86
|
+
}
|
|
61
87
|
|
|
62
88
|
return result;
|
|
63
89
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GraphQLError } from "graphql/error/index.js";
|
|
2
2
|
import { OpenAPIV3, type OpenAPIV3_1 } from "openapi-types";
|
|
3
|
-
import {
|
|
3
|
+
import { flattenAllOf } from "../../util/flattenAllOf.js";
|
|
4
|
+
import { traverse } from "../../util/traverse.js";
|
|
4
5
|
import { dereference, type JSONSchema } from "./dereference/index.js";
|
|
5
6
|
import { upgradeSchema } from "./upgrade/index.js";
|
|
6
7
|
|
|
@@ -104,11 +105,21 @@ export const validate = async (schemaInput: unknown) => {
|
|
|
104
105
|
const dereferenced = await dereference(schema);
|
|
105
106
|
const upgraded = upgradeSchema(dereferenced);
|
|
106
107
|
|
|
107
|
-
const flattened =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
const flattened = traverse(upgraded, (spec) => {
|
|
109
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
|
|
110
|
+
return spec;
|
|
111
|
+
}
|
|
112
|
+
const isSchemaObject =
|
|
113
|
+
"type" in spec ||
|
|
114
|
+
"properties" in spec ||
|
|
115
|
+
"allOf" in spec ||
|
|
116
|
+
"anyOf" in spec ||
|
|
117
|
+
"oneOf" in spec;
|
|
118
|
+
|
|
119
|
+
if (!isSchemaObject) return spec;
|
|
120
|
+
|
|
121
|
+
return flattenAllOf(spec) as typeof spec;
|
|
122
|
+
}) as OpenAPIDocument;
|
|
112
123
|
|
|
113
124
|
return flattened;
|
|
114
125
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { JSONSchema7 } from "json-schema";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
import type { OpenAPIDocument } from "../oas/parser/index.js";
|
|
4
|
-
import { flattenAllOf
|
|
4
|
+
import { flattenAllOf } from "./flattenAllOf.js";
|
|
5
|
+
import { flattenAllOfProcessor } from "./flattenAllOfProcessor.js";
|
|
5
6
|
import invariant from "./invariant.js";
|
|
6
7
|
|
|
7
8
|
describe("flattenAllOf", () => {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { $RefParser } from "@apidevtools/json-schema-ref-parser";
|
|
2
1
|
import {
|
|
3
2
|
createComparator,
|
|
4
3
|
createMerger,
|
|
@@ -9,62 +8,6 @@ import {
|
|
|
9
8
|
createIntersector,
|
|
10
9
|
} from "@x0k/json-schema-merge/lib/array";
|
|
11
10
|
import type { JSONSchema7Definition } from "json-schema";
|
|
12
|
-
import type { Processor } from "../../config/validators/BuildSchema.js";
|
|
13
|
-
import type { OpenAPIDocument } from "../oas/parser/index.js";
|
|
14
|
-
import { type RecordAny, traverse } from "./traverse.js";
|
|
15
|
-
|
|
16
|
-
export const flattenAllOfProcessor: Processor = async ({ schema, file }) => {
|
|
17
|
-
try {
|
|
18
|
-
// Resolve refs once - creates a lookup table without modifying the schema
|
|
19
|
-
const parser = new $RefParser();
|
|
20
|
-
await parser.resolve(schema);
|
|
21
|
-
const $refs = parser.$refs;
|
|
22
|
-
|
|
23
|
-
const flattened = traverse(schema, (spec) => {
|
|
24
|
-
if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
|
|
25
|
-
return spec;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const isSchemaObject =
|
|
29
|
-
"type" in spec ||
|
|
30
|
-
"properties" in spec ||
|
|
31
|
-
"allOf" in spec ||
|
|
32
|
-
"anyOf" in spec ||
|
|
33
|
-
"oneOf" in spec;
|
|
34
|
-
|
|
35
|
-
if (!isSchemaObject) return spec;
|
|
36
|
-
|
|
37
|
-
if ("allOf" in spec && Array.isArray(spec.allOf)) {
|
|
38
|
-
const resolvedAllOf = spec.allOf.map((item) => {
|
|
39
|
-
if (
|
|
40
|
-
item &&
|
|
41
|
-
typeof item === "object" &&
|
|
42
|
-
"$ref" in item &&
|
|
43
|
-
typeof item.$ref === "string"
|
|
44
|
-
) {
|
|
45
|
-
try {
|
|
46
|
-
return $refs.get(item.$ref) ?? item;
|
|
47
|
-
} catch {
|
|
48
|
-
return item;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return item;
|
|
52
|
-
});
|
|
53
|
-
return flattenAllOf({ ...spec, allOf: resolvedAllOf }) as RecordAny;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return flattenAllOf(spec) as RecordAny;
|
|
57
|
-
}) as OpenAPIDocument;
|
|
58
|
-
|
|
59
|
-
return flattened;
|
|
60
|
-
} catch (error) {
|
|
61
|
-
// biome-ignore lint/suspicious/noConsole: Logging allowed here
|
|
62
|
-
console.warn(
|
|
63
|
-
`Failed to flatten \`allOf\` in ${file}: ${error instanceof Error ? error.message : error}`,
|
|
64
|
-
);
|
|
65
|
-
return schema;
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
11
|
|
|
69
12
|
const { compareSchemaDefinitions, compareSchemaValues } = createComparator();
|
|
70
13
|
const { mergeArrayOfSchemaDefinitions } = createMerger({
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { $RefParser } from "@apidevtools/json-schema-ref-parser";
|
|
2
|
+
import type { Processor } from "../../config/validators/BuildSchema.js";
|
|
3
|
+
import type { OpenAPIDocument } from "../oas/parser/index.js";
|
|
4
|
+
import { flattenAllOf } from "./flattenAllOf.js";
|
|
5
|
+
import { type RecordAny, traverse } from "./traverse.js";
|
|
6
|
+
|
|
7
|
+
export const flattenAllOfProcessor: Processor = async ({ schema, file }) => {
|
|
8
|
+
try {
|
|
9
|
+
// Resolve refs once - creates a lookup table without modifying the schema
|
|
10
|
+
const parser = new $RefParser();
|
|
11
|
+
await parser.resolve(schema);
|
|
12
|
+
const $refs = parser.$refs;
|
|
13
|
+
|
|
14
|
+
const flattened = traverse(schema, (spec) => {
|
|
15
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
|
|
16
|
+
return spec;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isSchemaObject =
|
|
20
|
+
"type" in spec ||
|
|
21
|
+
"properties" in spec ||
|
|
22
|
+
"allOf" in spec ||
|
|
23
|
+
"anyOf" in spec ||
|
|
24
|
+
"oneOf" in spec;
|
|
25
|
+
|
|
26
|
+
if (!isSchemaObject) return spec;
|
|
27
|
+
|
|
28
|
+
if ("allOf" in spec && Array.isArray(spec.allOf)) {
|
|
29
|
+
const resolvedAllOf = spec.allOf.map((item) => {
|
|
30
|
+
if (
|
|
31
|
+
item &&
|
|
32
|
+
typeof item === "object" &&
|
|
33
|
+
"$ref" in item &&
|
|
34
|
+
typeof item.$ref === "string"
|
|
35
|
+
) {
|
|
36
|
+
try {
|
|
37
|
+
return $refs.get(item.$ref) ?? item;
|
|
38
|
+
} catch {
|
|
39
|
+
return item;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return item;
|
|
43
|
+
});
|
|
44
|
+
return flattenAllOf({ ...spec, allOf: resolvedAllOf }) as RecordAny;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return flattenAllOf(spec) as RecordAny;
|
|
48
|
+
}) as OpenAPIDocument;
|
|
49
|
+
|
|
50
|
+
return flattened;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// biome-ignore lint/suspicious/noConsole: Logging allowed here
|
|
53
|
+
console.warn(
|
|
54
|
+
`Failed to flatten \`allOf\` in ${file}: ${error instanceof Error ? error.message : error}`,
|
|
55
|
+
);
|
|
56
|
+
return schema;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
@@ -9,5 +9,6 @@ export const yaml = {
|
|
|
9
9
|
|
|
10
10
|
export const readFrontmatter = async (filePath: string) => {
|
|
11
11
|
const content = await readFile(filePath, "utf-8");
|
|
12
|
-
|
|
12
|
+
const normalizedContent = content.replace(/\r\n/g, "\n");
|
|
13
|
+
return matter(normalizedContent, { engines: { yaml } });
|
|
13
14
|
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
ExtensionMcpServer,
|
|
5
|
+
ExtensionMcpServerTool,
|
|
6
|
+
} from "@zuplo/mcp/openapi/types";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_MCP_SERVER_NAME,
|
|
9
|
+
DEFAULT_MCP_SERVER_VERSION,
|
|
10
|
+
} from "@zuplo/mcp/server";
|
|
11
|
+
import type { OpenAPIV3_1 } from "openapi-types";
|
|
12
|
+
import type { ProcessorArg } from "../config/validators/BuildSchema.js";
|
|
13
|
+
import { traverse, traverseAsync } from "../lib/util/traverse.js";
|
|
14
|
+
import type { RecordAny } from "../lib/util/types.js";
|
|
15
|
+
import { operations } from "./enrich-with-zuplo.js";
|
|
16
|
+
|
|
17
|
+
// extracts x-mcp-server metadata from the operation using x-zuplo-mcp-tool
|
|
18
|
+
// as a first class citizen.
|
|
19
|
+
const extractOperationSchema = (
|
|
20
|
+
operation: OpenAPIV3_1.OperationObject & RecordAny,
|
|
21
|
+
): ExtensionMcpServerTool | null => {
|
|
22
|
+
if (!operation.operationId) return null;
|
|
23
|
+
|
|
24
|
+
// Check if tool is explicitly disabled
|
|
25
|
+
const mcpToolConfig = operation["x-zuplo-mcp-tool"];
|
|
26
|
+
if (mcpToolConfig?.enabled === false) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tool: ExtensionMcpServerTool = {
|
|
31
|
+
// Use custom name from x-zuplo-mcp-tool or fallback to operationId
|
|
32
|
+
name: mcpToolConfig?.name || operation.operationId,
|
|
33
|
+
|
|
34
|
+
// Use custom description from x-zuplo-mcp-tool or fallback
|
|
35
|
+
// to operation description
|
|
36
|
+
description:
|
|
37
|
+
mcpToolConfig?.description ||
|
|
38
|
+
operation.summary ||
|
|
39
|
+
operation.description ||
|
|
40
|
+
`Operation ${operation.operationId}`,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Grab valid request body JSON schema for the tool
|
|
44
|
+
const requestBody = operation.requestBody as
|
|
45
|
+
| OpenAPIV3_1.RequestBodyObject
|
|
46
|
+
| undefined;
|
|
47
|
+
|
|
48
|
+
const schema = requestBody?.content?.["application/json"]?.schema;
|
|
49
|
+
if (schema && typeof schema === "object") {
|
|
50
|
+
// TODO: @jpmcb - Zuplo also supports in-path parameters and query parameters
|
|
51
|
+
// as MCP "inputSchema" arguments. In order to document full argument params,
|
|
52
|
+
// Zudoku will need to more intelligently parse these elements of an operation.
|
|
53
|
+
tool.inputSchema = { body: schema };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return tool;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Builds a lookup map of operationId -> operation for efficient access
|
|
60
|
+
const buildOperationLookup = (
|
|
61
|
+
document: OpenAPIV3_1.Document,
|
|
62
|
+
): Map<string, OpenAPIV3_1.OperationObject> => {
|
|
63
|
+
const operationMap = new Map<string, OpenAPIV3_1.OperationObject>();
|
|
64
|
+
|
|
65
|
+
traverse(document, (node, path) => {
|
|
66
|
+
// Check if we're at a path item level (paths -> /some/path -> method)
|
|
67
|
+
// and validate it's in allowed operations
|
|
68
|
+
if (
|
|
69
|
+
!path ||
|
|
70
|
+
path.length < 2 ||
|
|
71
|
+
path[0] !== "paths" ||
|
|
72
|
+
!operations.includes(path[path.length - 1] as string)
|
|
73
|
+
) {
|
|
74
|
+
return node;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (node.operationId) {
|
|
78
|
+
operationMap.set(node.operationId, node);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return node;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return operationMap;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Takes an OpenAPI document and returns the x-mcp-server tools list defined
|
|
88
|
+
// by an MCP server's options.files[x].operationIds array.
|
|
89
|
+
const findOperationsInDocument = (
|
|
90
|
+
document: OpenAPIV3_1.Document,
|
|
91
|
+
operationIds: string[],
|
|
92
|
+
): ExtensionMcpServerTool[] => {
|
|
93
|
+
const tools: ExtensionMcpServerTool[] = [];
|
|
94
|
+
const operationLookup = buildOperationLookup(document);
|
|
95
|
+
|
|
96
|
+
operationIds.forEach((operationId) => {
|
|
97
|
+
const operation = operationLookup.get(operationId);
|
|
98
|
+
if (operation) {
|
|
99
|
+
const tool = extractOperationSchema(operation);
|
|
100
|
+
if (tool) {
|
|
101
|
+
tools.push(tool);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return tools;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Enriches an OpenAPI schema with x-mcp-server data based on the Zuplo MCP server handler
|
|
110
|
+
export const enrichWithZuploMcpServerData = ({
|
|
111
|
+
rootDir,
|
|
112
|
+
}: {
|
|
113
|
+
rootDir: string;
|
|
114
|
+
}) => {
|
|
115
|
+
return async ({ schema }: ProcessorArg) => {
|
|
116
|
+
if (!schema.paths) return schema;
|
|
117
|
+
const modifiedSchema = { ...schema };
|
|
118
|
+
if (!modifiedSchema?.paths) return modifiedSchema;
|
|
119
|
+
|
|
120
|
+
await traverseAsync(modifiedSchema, async (node, nodePath) => {
|
|
121
|
+
// Check if we're at a "post" operation (paths -> /some/path -> "post").
|
|
122
|
+
// HTTP MCP servers are only allow post operations.
|
|
123
|
+
if (!nodePath || nodePath.length !== 3 || nodePath[2] !== "post") {
|
|
124
|
+
return node;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const operation = node as RecordAny;
|
|
128
|
+
if (!operation?.["x-zuplo-route"]) return node;
|
|
129
|
+
|
|
130
|
+
const handler = operation["x-zuplo-route"]?.handler;
|
|
131
|
+
if (handler?.export !== "mcpServerHandler" || !handler.options?.files)
|
|
132
|
+
return node;
|
|
133
|
+
|
|
134
|
+
const tools: ExtensionMcpServerTool[] = [];
|
|
135
|
+
|
|
136
|
+
for (const fileConfig of handler.options.files) {
|
|
137
|
+
if (!fileConfig.path || !fileConfig.operationIds) continue;
|
|
138
|
+
|
|
139
|
+
const resolvedPath = path.resolve(rootDir, "../", fileConfig.path);
|
|
140
|
+
const fileContent = await fs.readFile(resolvedPath, "utf-8");
|
|
141
|
+
const document = JSON.parse(fileContent);
|
|
142
|
+
|
|
143
|
+
if (document) {
|
|
144
|
+
const fileTools = findOperationsInDocument(
|
|
145
|
+
document,
|
|
146
|
+
fileConfig.operationIds,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
tools.push(...fileTools);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const mcpExtension: ExtensionMcpServer = {
|
|
154
|
+
name: DEFAULT_MCP_SERVER_NAME,
|
|
155
|
+
version: DEFAULT_MCP_SERVER_VERSION,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (tools.length > 0) {
|
|
159
|
+
mcpExtension.tools = tools;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
node["x-mcp-server"] = mcpExtension;
|
|
163
|
+
return node;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return modifiedSchema;
|
|
167
|
+
};
|
|
168
|
+
};
|