@scalar/openapi-to-markdown 1.1.0 → 1.2.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/CHANGELOG.md +12 -0
- package/dist/get-markdown-examples.d.ts +7 -0
- package/dist/get-markdown-examples.d.ts.map +1 -1
- package/dist/get-markdown-examples.js +68 -0
- package/dist/render-document.d.ts.map +1 -1
- package/dist/render-document.js +6 -2
- package/dist/render-examples.d.ts.map +1 -1
- package/dist/render-examples.js +4 -0
- package/dist/render-operation.d.ts.map +1 -1
- package/dist/render-operation.js +4 -5
- package/dist/render-schema.d.ts +20 -2
- package/dist/render-schema.d.ts.map +1 -1
- package/dist/render-schema.js +175 -67
- package/dist/select-document.d.ts +3 -3
- package/dist/select-document.d.ts.map +1 -1
- package/dist/select-document.js +56 -35
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @scalar/openapi-to-markdown
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#10186](https://github.com/scalar/scalar/pull/10186): Support document-defined additional operations in Markdown operation, webhook, and tag selections. Preserve custom method spelling and inherited parameters, servers, and security when copying an operation as Markdown.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#10329](https://github.com/scalar/scalar/pull/10329): Render each shared schema once per page. The first reference to a structured schema expands it and labels it with its name (for example `schema: Account`). Later references print "Schema `Account` is shown above." instead of expanding it again. Model sections for schemas already expanded on the page refer back to them too. True cycles still print `[Circular Reference]`. This keeps Markdown output proportional to the schema graph, rather than to the number of paths through it. Before this change, densely linked descriptions such as Stripe's ran out of memory when rendering a single operation.
|
|
12
|
+
|
|
13
|
+
A node budget per operation and model section adds a visible `[Schema output truncated]` marker as a last resort. Generated examples that would exceed 10,000 values are omitted with a note. References past the depth limit now point to the model's own section instead of being cut off.
|
|
14
|
+
|
|
3
15
|
## 1.1.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -15,7 +15,14 @@ type MarkdownExample = {
|
|
|
15
15
|
externalValue: string;
|
|
16
16
|
} | {
|
|
17
17
|
serializedValue: string;
|
|
18
|
+
} | {
|
|
19
|
+
omitted: true;
|
|
18
20
|
});
|
|
21
|
+
/**
|
|
22
|
+
* Estimate how many values an example generated from this schema contains, stopping just past the limit.
|
|
23
|
+
* Counts are cached per schema and level, so this is linear in the size of the schema graph.
|
|
24
|
+
*/
|
|
25
|
+
export declare const countGeneratedExampleValues: (root: unknown, limit?: number) => number;
|
|
19
26
|
/** Preserve supplied values; generate a fallback only when examples are not supplied. */
|
|
20
27
|
export declare const getMarkdownExamples: (source: ExampleSource, mediaType: string, mode?: "read" | "write", openapiVersion?: string) => MarkdownExample[];
|
|
21
28
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-markdown-examples.d.ts","sourceRoot":"","sources":["../src/get-markdown-examples.ts"],"names":[],"mappings":"AAKA,4FAA4F;AAC5F,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC,CAAA;AAED,gFAAgF;AAChF,KAAK,eAAe,GAAG;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,CAAC;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,eAAe,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"get-markdown-examples.d.ts","sourceRoot":"","sources":["../src/get-markdown-examples.ts"],"names":[],"mappings":"AAKA,4FAA4F;AAC5F,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC,CAAA;AAED,gFAAgF;AAChF,KAAK,eAAe,GAAG;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,CAAC;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,eAAe,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,CAAC,CAAA;AAYtG;;;GAGG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,OAAO,EAAE,cAAoC,KAAG,MA2CjG,CAAA;AAED,yFAAyF;AACzF,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,aAAa,EACrB,WAAW,MAAM,EACjB,OAAO,MAAM,GAAG,OAAO,EACvB,uBAAwB,KACvB,eAAe,EA4BjB,CAAA"}
|
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
import { isObject } from '@scalar/helpers/object/is-object';
|
|
2
2
|
import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
3
3
|
import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
|
|
4
|
+
/** Mirrors the depth at which `getExampleFromSchema` stops following nested schemas. */
|
|
5
|
+
const EXAMPLE_DEPTH = 10;
|
|
6
|
+
/**
|
|
7
|
+
* Generated examples repeat every shared schema at each place it is used, up to ten levels deep.
|
|
8
|
+
* A densely shared schema graph therefore produces billions of values, so larger examples are skipped.
|
|
9
|
+
* The largest generated examples in the Stripe, GitHub, and Cloudflare descriptions have about 7,600 values.
|
|
10
|
+
*/
|
|
11
|
+
const MAX_GENERATED_EXAMPLE_VALUES = 10_000;
|
|
12
|
+
/**
|
|
13
|
+
* Estimate how many values an example generated from this schema contains, stopping just past the limit.
|
|
14
|
+
* Counts are cached per schema and level, so this is linear in the size of the schema graph.
|
|
15
|
+
*/
|
|
16
|
+
export const countGeneratedExampleValues = (root, limit = MAX_GENERATED_EXAMPLE_VALUES) => {
|
|
17
|
+
const counts = new WeakMap();
|
|
18
|
+
const count = (input, level) => {
|
|
19
|
+
if (level > EXAMPLE_DEPTH || !isObject(input))
|
|
20
|
+
return 1;
|
|
21
|
+
const cached = counts.get(input)?.get(level);
|
|
22
|
+
if (cached !== undefined)
|
|
23
|
+
return cached;
|
|
24
|
+
const schema = getResolvedRef(input, mergeSiblingReferences);
|
|
25
|
+
if (!isObject(schema))
|
|
26
|
+
return 1;
|
|
27
|
+
// Supplied examples bypass schema expansion in the generator too.
|
|
28
|
+
if (schema.example !== undefined || (Array.isArray(schema.examples) && schema.examples.length > 0))
|
|
29
|
+
return 1;
|
|
30
|
+
let total = 1;
|
|
31
|
+
const add = (child) => {
|
|
32
|
+
if (total <= limit)
|
|
33
|
+
total += count(child, level + 1);
|
|
34
|
+
};
|
|
35
|
+
if (isObject(schema.properties))
|
|
36
|
+
Object.values(schema.properties).forEach(add);
|
|
37
|
+
if (isObject(schema.patternProperties))
|
|
38
|
+
Object.values(schema.patternProperties).forEach(add);
|
|
39
|
+
if (isObject(schema.additionalProperties))
|
|
40
|
+
add(schema.additionalProperties);
|
|
41
|
+
if (schema.items !== undefined)
|
|
42
|
+
add(schema.items);
|
|
43
|
+
if (Array.isArray(schema.prefixItems))
|
|
44
|
+
schema.prefixItems.forEach(add);
|
|
45
|
+
if (Array.isArray(schema.allOf))
|
|
46
|
+
schema.allOf.forEach(add);
|
|
47
|
+
const variants = Array.isArray(schema.oneOf) ? schema.oneOf : Array.isArray(schema.anyOf) ? schema.anyOf : [];
|
|
48
|
+
// Object and array generation can use the first variant, while other unions skip null.
|
|
49
|
+
// A discriminator can choose any variant, so bound the largest candidate in that case.
|
|
50
|
+
const nonNull = variants.find((variant) => {
|
|
51
|
+
const resolved = getResolvedRef(variant, mergeSiblingReferences);
|
|
52
|
+
return isObject(resolved) && resolved.type !== 'null';
|
|
53
|
+
});
|
|
54
|
+
const candidates = isObject(schema.discriminator) && schema.discriminator.defaultMapping !== undefined
|
|
55
|
+
? variants
|
|
56
|
+
: [variants[0], nonNull];
|
|
57
|
+
let variantCount = 0;
|
|
58
|
+
for (const candidate of candidates) {
|
|
59
|
+
if (candidate !== undefined && total <= limit && variantCount <= limit)
|
|
60
|
+
variantCount = Math.max(variantCount, count(candidate, level + 1));
|
|
61
|
+
}
|
|
62
|
+
total += variantCount;
|
|
63
|
+
const levels = counts.get(input) ?? new Map();
|
|
64
|
+
levels.set(level, total);
|
|
65
|
+
counts.set(input, levels);
|
|
66
|
+
return total;
|
|
67
|
+
};
|
|
68
|
+
return count(root, 0);
|
|
69
|
+
};
|
|
4
70
|
/** Preserve supplied values; generate a fallback only when examples are not supplied. */
|
|
5
71
|
export const getMarkdownExamples = (source, mediaType, mode, openapiVersion = '3.2.0') => {
|
|
6
72
|
if (source.example !== undefined)
|
|
@@ -29,6 +95,8 @@ export const getMarkdownExamples = (source, mediaType, mode, openapiVersion = '3
|
|
|
29
95
|
const schema = getResolvedRef(source.schema);
|
|
30
96
|
if (!isObject(schema))
|
|
31
97
|
return [];
|
|
98
|
+
if (countGeneratedExampleValues(source.schema) > MAX_GENERATED_EXAMPLE_VALUES)
|
|
99
|
+
return [{ omitted: true }];
|
|
32
100
|
const value = getExampleFromSchema(getResolvedRef(source.schema, mergeSiblingReferences), {
|
|
33
101
|
xml: mediaType.includes('xml'),
|
|
34
102
|
mode,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-document.d.ts","sourceRoot":"","sources":["../src/render-document.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAmB,MAAM,8DAA8D,CAAA;AAepH,2FAA2F;AAC3F,eAAO,MAAM,sBAAsB,QAAO,CAAC,CAAC,QAAQ,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"render-document.d.ts","sourceRoot":"","sources":["../src/render-document.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAmB,MAAM,8DAA8D,CAAA;AAepH,2FAA2F;AAC3F,eAAO,MAAM,sBAAsB,QAAO,CAAC,CAAC,QAAQ,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,CA6IxF,CAAA"}
|
package/dist/render-document.js
CHANGED
|
@@ -13,9 +13,11 @@ const serializer = unified().use(remarkGfm).use(remarkStringify, { bullet: '-' }
|
|
|
13
13
|
/** Build Markdown directly, retaining caches only for this immutable document snapshot. */
|
|
14
14
|
export const createDocumentRenderer = () => {
|
|
15
15
|
const descriptions = createDescriptionParser();
|
|
16
|
-
const
|
|
16
|
+
const schemaRenderer = createSchemaRenderer();
|
|
17
17
|
return async (document) => {
|
|
18
18
|
const description = descriptions();
|
|
19
|
+
// Each page expands a shared schema once, then refers back to it.
|
|
20
|
+
const schemas = schemaRenderer.forDocument(document.components?.schemas);
|
|
19
21
|
const { info } = document;
|
|
20
22
|
const metadata = [
|
|
21
23
|
field('OpenAPI Version', inlineCode(document.openapi)),
|
|
@@ -85,6 +87,7 @@ export const createDocumentRenderer = () => {
|
|
|
85
87
|
nodes.push(heading(2, text(group.title)));
|
|
86
88
|
hasOperations = true;
|
|
87
89
|
}
|
|
90
|
+
schemas.beginSection();
|
|
88
91
|
nodes.push(...(await renderOperation(document, path, method, pathItem, operation, group.webhook, {
|
|
89
92
|
description,
|
|
90
93
|
schemas,
|
|
@@ -97,12 +100,13 @@ export const createDocumentRenderer = () => {
|
|
|
97
100
|
if (models.length)
|
|
98
101
|
nodes.push(heading(2, text('Schemas')));
|
|
99
102
|
for (const [name, schema] of models) {
|
|
103
|
+
schemas.beginSection();
|
|
100
104
|
const view = schemas.view(schema);
|
|
101
105
|
nodes.push(heading(3, text(view.title ?? name)), list([
|
|
102
106
|
view.type
|
|
103
107
|
? field('Type', inlineCode(Array.isArray(view.type) ? view.type.join(' | ') : view.type))
|
|
104
108
|
: item(paragraph(strong(text('Type:')))),
|
|
105
|
-
]), ...(await description(view.description)), ...schemas.render(schema, 0, [], { hideDescription: true }));
|
|
109
|
+
]), ...(await description(view.description)), ...schemas.render(schema, 0, [], { hideDescription: true, name }));
|
|
106
110
|
if (view.type === 'object')
|
|
107
111
|
nodes.push(...(await renderExamples({ schema }, description, 'application/json', undefined, document['x-original-oas-version'] ?? document.openapi)));
|
|
108
112
|
flush();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-examples.d.ts","sourceRoot":"","sources":["../src/render-examples.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAExC,OAAO,EAAE,KAAK,aAAa,EAAuB,MAAM,yBAAyB,CAAA;AAEjF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE5D,+EAA+E;AAC/E,eAAO,MAAM,cAAc,GACzB,QAAQ,aAAa,EACrB,aAAa,iBAAiB,EAC9B,kBAA8B,EAC9B,OAAO,MAAM,GAAG,OAAO,EACvB,uBAAwB,KACvB,OAAO,CAAC,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"render-examples.d.ts","sourceRoot":"","sources":["../src/render-examples.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAExC,OAAO,EAAE,KAAK,aAAa,EAAuB,MAAM,yBAAyB,CAAA;AAEjF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE5D,+EAA+E;AAC/E,eAAO,MAAM,cAAc,GACzB,QAAQ,aAAa,EACrB,aAAa,iBAAiB,EAC9B,kBAA8B,EAC9B,OAAO,MAAM,GAAG,OAAO,EACvB,uBAAwB,KACvB,OAAO,CAAC,WAAW,EAAE,CAqCvB,CAAA"}
|
package/dist/render-examples.js
CHANGED
|
@@ -6,6 +6,10 @@ export const renderExamples = async (source, description, mediaType = 'applicati
|
|
|
6
6
|
const nodes = [];
|
|
7
7
|
for (const example of getMarkdownExamples(source, mediaType, mode, openapiVersion)) {
|
|
8
8
|
nodes.push(paragraph(strong(text(example.name ? `Example: ${example.name}` : 'Example:'))));
|
|
9
|
+
if ('omitted' in example) {
|
|
10
|
+
nodes.push(paragraph(text('[Generated example omitted because it is too large]')));
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
9
13
|
if (example.summary)
|
|
10
14
|
nodes.push(paragraph(text(example.summary)));
|
|
11
15
|
nodes.push(...(await description(example.description)));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-operation.d.ts","sourceRoot":"","sources":["../src/render-operation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-operation.d.ts","sourceRoot":"","sources":["../src/render-operation.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EAEf,cAAc,EAGf,MAAM,8DAA8D,CAAA;AACrE,OAAO,KAAK,EAAY,WAAW,EAAE,MAAM,OAAO,CAAA;AAGlD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAG5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAGrD,oEAAoE;AACpE,KAAK,aAAa,GAAG;IACnB,WAAW,EAAE,iBAAiB,CAAA;IAC9B,OAAO,EAAE,cAAc,CAAA;CACxB,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,eAAe,GAC1B,UAAU,eAAe,EACzB,MAAM,MAAM,EACZ,QAAQ,MAAM,EACd,UAAU,cAAc,EACxB,WAAW,eAAe,EAC1B,SAAS,OAAO,EAChB,0BAA0B,aAAa,KACtC,OAAO,CAAC,WAAW,EAAE,CAyGvB,CAAA"}
|
package/dist/render-operation.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isHttpMethod } from '@scalar/helpers/http/is-http-method';
|
|
1
2
|
import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
2
3
|
import { field, heading, inlineCode, item, list, paragraph, strong, text } from './markdown-nodes.js';
|
|
3
4
|
import { renderExamples } from './render-examples.js';
|
|
@@ -5,14 +6,12 @@ import { renderEncoding, renderHeaders, renderResponseLinks } from './render-ope
|
|
|
5
6
|
import { renderSecurity } from './render-security.js';
|
|
6
7
|
/** Render effective operation context without mutating the prepared document. */
|
|
7
8
|
export const renderOperation = async (document, path, method, pathItem, operation, webhook, { description, schemas }) => {
|
|
9
|
+
const displayMethod = method === method.toLowerCase() && isHttpMethod(method) ? method.toUpperCase() : method;
|
|
8
10
|
const openapiVersion = document['x-original-oas-version'] ?? document.openapi;
|
|
9
11
|
const stability = operation['x-scalar-stability'];
|
|
10
|
-
const title = (operation.summary || `${
|
|
12
|
+
const title = (operation.summary || `${displayMethod} ${path}`) +
|
|
11
13
|
(stability ? ` (${stability})` : operation.deprecated ? ' ⚠️ Deprecated' : '');
|
|
12
|
-
const metadata = [
|
|
13
|
-
field('Method', inlineCode(method.toUpperCase())),
|
|
14
|
-
field(webhook ? 'Webhook' : 'Path', inlineCode(path)),
|
|
15
|
-
];
|
|
14
|
+
const metadata = [field('Method', inlineCode(displayMethod)), field(webhook ? 'Webhook' : 'Path', inlineCode(path))];
|
|
16
15
|
if (operation.operationId)
|
|
17
16
|
metadata.push(field('Operation ID', inlineCode(operation.operationId)));
|
|
18
17
|
if (operation.tags)
|
package/dist/render-schema.d.ts
CHANGED
|
@@ -38,18 +38,36 @@ type SchemaView = {
|
|
|
38
38
|
propertyName: string;
|
|
39
39
|
mapping?: Record<string, string>;
|
|
40
40
|
};
|
|
41
|
+
/** Set for references to a structured schema that is rendered once per document and referred to afterwards. */
|
|
42
|
+
name?: string;
|
|
41
43
|
};
|
|
42
44
|
type RenderOptions = {
|
|
43
45
|
hideDescription?: boolean;
|
|
44
46
|
hideDetails?: boolean;
|
|
45
47
|
property?: boolean;
|
|
48
|
+
/** Record this root under the given name, for example in its own model section, even if it is not a reference. */
|
|
49
|
+
name?: string;
|
|
46
50
|
};
|
|
47
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Schema normalization is cached separately from ancestry-dependent expansion.
|
|
53
|
+
*
|
|
54
|
+
* A renderer also tracks which shared schemas one output document already shows, so each one is
|
|
55
|
+
* expanded once and referred to afterwards. Use `forDocument` for every document: renders can
|
|
56
|
+
* run concurrently, and each needs its own record.
|
|
57
|
+
*/
|
|
48
58
|
export type SchemaRenderer = {
|
|
49
59
|
view: (schema: MarkdownSchema) => SchemaView;
|
|
50
60
|
render: (schema: MarkdownSchema, depth?: number, ancestors?: readonly unknown[], options?: RenderOptions) => RootContent[];
|
|
61
|
+
/** Restore the node budget, for example for each operation or model section. */
|
|
62
|
+
beginSection: () => void;
|
|
63
|
+
/** Start a document that shares normalized views, given the models it renders in its own sections. */
|
|
64
|
+
forDocument: (models?: Record<string, MarkdownSchema>) => SchemaRenderer;
|
|
65
|
+
};
|
|
66
|
+
type SchemaRendererOptions = {
|
|
67
|
+
/** Upper bound on expanded schema nodes per section, so no description can make a page unbounded. */
|
|
68
|
+
maxNodes?: number;
|
|
51
69
|
};
|
|
52
70
|
/** Keep merged reference siblings and sorted properties stable throughout an export. */
|
|
53
|
-
export declare const createSchemaRenderer: () => SchemaRenderer;
|
|
71
|
+
export declare const createSchemaRenderer: ({ maxNodes }?: SchemaRendererOptions) => SchemaRenderer;
|
|
54
72
|
export {};
|
|
55
73
|
//# sourceMappingURL=render-schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-schema.d.ts","sourceRoot":"","sources":["../src/render-schema.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-schema.d.ts","sourceRoot":"","sources":["../src/render-schema.ts"],"names":[],"mappings":"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"}
|
package/dist/render-schema.js
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
|
+
import { unescapeJsonPointer } from '@scalar/helpers/json/unescape-json-pointer';
|
|
1
2
|
import { isObject } from '@scalar/helpers/object/is-object';
|
|
2
3
|
import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
3
4
|
import { inlineCode, item, list, paragraph, strong, text } from './markdown-nodes.js';
|
|
5
|
+
/** Guards against stack exhaustion. Past it, references to models point to their own section instead. */
|
|
6
|
+
const MAX_DEPTH = 64;
|
|
7
|
+
/**
|
|
8
|
+
* Deduplicated output is proportional to the schema graph, so only a pathological description
|
|
9
|
+
* (for example a YAML alias graph without references) reaches this.
|
|
10
|
+
*/
|
|
11
|
+
const MAX_NODES = 100_000;
|
|
12
|
+
/** Keywords that `render` expands, as opposed to annotations that `details` prints on one line. */
|
|
13
|
+
const structuralKeywords = new Set([
|
|
14
|
+
'allOf',
|
|
15
|
+
'anyOf',
|
|
16
|
+
'oneOf',
|
|
17
|
+
'not',
|
|
18
|
+
'properties',
|
|
19
|
+
'required',
|
|
20
|
+
'items',
|
|
21
|
+
'additionalProperties',
|
|
22
|
+
'discriminator',
|
|
23
|
+
'minItems',
|
|
24
|
+
'maxItems',
|
|
25
|
+
'uniqueItems',
|
|
26
|
+
]);
|
|
27
|
+
/** Link bookkeeping that is not a sibling keyword of a reference. */
|
|
28
|
+
const referenceKeys = new Set(['$ref', '$ref-value', '$global', '$status']);
|
|
29
|
+
const emphasis = (...children) => ({ type: 'emphasis', children });
|
|
30
|
+
/** Prefer the component name, which is how model sections and other references identify the schema. */
|
|
31
|
+
const getReferenceName = (ref) => {
|
|
32
|
+
const match = /^#\/components\/schemas\/([^/]+)$/.exec(ref);
|
|
33
|
+
if (!match)
|
|
34
|
+
return ref;
|
|
35
|
+
try {
|
|
36
|
+
return unescapeJsonPointer(match[1]);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return ref;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/** Only references that add nothing structural can be replaced by the schema they point to. */
|
|
43
|
+
const getSharedName = (input, view) => {
|
|
44
|
+
const ref = isObject(input) ? input.$ref : undefined;
|
|
45
|
+
if (typeof ref !== 'string' || typeof view.schema !== 'object')
|
|
46
|
+
return undefined;
|
|
47
|
+
if (Object.keys(input).some((key) => structuralKeywords.has(key)))
|
|
48
|
+
return undefined;
|
|
49
|
+
const structured = Boolean(view.allOf?.length || view.anyOf?.length || view.oneOf?.length || view.properties.length) ||
|
|
50
|
+
view.not !== undefined ||
|
|
51
|
+
view.items !== undefined ||
|
|
52
|
+
view.additionalProperties !== undefined;
|
|
53
|
+
// Leaf schemas are as short as a reference to them, so they stay in place.
|
|
54
|
+
return structured ? getReferenceName(ref) : undefined;
|
|
55
|
+
};
|
|
4
56
|
/** Boolean targets still combine with adjacent schema keywords. */
|
|
5
57
|
const resolveMarkdownSchema = (input) => {
|
|
6
58
|
const target = getResolvedRef(input);
|
|
@@ -16,7 +68,7 @@ const resolveMarkdownSchema = (input) => {
|
|
|
16
68
|
return { ...merged, allOf: [false, ...(Array.isArray(merged.allOf) ? merged.allOf : [])] };
|
|
17
69
|
};
|
|
18
70
|
/** Keep merged reference siblings and sorted properties stable throughout an export. */
|
|
19
|
-
export const createSchemaRenderer = () => {
|
|
71
|
+
export const createSchemaRenderer = ({ maxNodes = MAX_NODES } = {}) => {
|
|
20
72
|
const views = new WeakMap();
|
|
21
73
|
const view = (input) => {
|
|
22
74
|
const cached = typeof input === 'object' ? views.get(input) : undefined;
|
|
@@ -37,6 +89,7 @@ export const createSchemaRenderer = () => {
|
|
|
37
89
|
required,
|
|
38
90
|
properties,
|
|
39
91
|
};
|
|
92
|
+
result.name = getSharedName(input, result);
|
|
40
93
|
if (typeof input === 'object')
|
|
41
94
|
views.set(input, result);
|
|
42
95
|
return result;
|
|
@@ -50,6 +103,8 @@ export const createSchemaRenderer = () => {
|
|
|
50
103
|
if (entry !== undefined)
|
|
51
104
|
nodes.push(text(`${nodes.length ? ', ' : ''}${label}: `), inlineCode(entry));
|
|
52
105
|
};
|
|
106
|
+
// Later references to a shared schema point back to this name.
|
|
107
|
+
add('schema', value.name);
|
|
53
108
|
add('format', value.format);
|
|
54
109
|
if (value.enum)
|
|
55
110
|
add('possible values', value.enum.map((entry) => JSON.stringify(entry)).join(', '));
|
|
@@ -78,71 +133,124 @@ export const createSchemaRenderer = () => {
|
|
|
78
133
|
nodes.push(text(`${nodes.length ? ' — ' : ''}${value.description}`));
|
|
79
134
|
return nodes;
|
|
80
135
|
};
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
136
|
+
const forDocument = (models = {}) => {
|
|
137
|
+
/** Shared schemas this document already expanded, with the name later references use. */
|
|
138
|
+
const shown = new Map();
|
|
139
|
+
/** Models that the document renders in their own sections after the operations. */
|
|
140
|
+
const sections = new Set(Object.values(models).map((model) => getResolvedRef(model) ?? model));
|
|
141
|
+
let nodeCount = 0;
|
|
142
|
+
/** Refer to a schema expanded elsewhere, keeping annotations the reference itself adds. */
|
|
143
|
+
const reference = (input, value, options, name, location) => {
|
|
144
|
+
const nodes = [];
|
|
145
|
+
const siblings = isObject(input) && Object.keys(input).some((key) => !referenceKeys.has(key));
|
|
146
|
+
if (siblings && !options.hideDetails) {
|
|
147
|
+
const annotations = details({ ...value, name: undefined }, false, options.hideDescription);
|
|
148
|
+
if (annotations.length)
|
|
149
|
+
nodes.push(paragraph(...annotations));
|
|
150
|
+
}
|
|
151
|
+
nodes.push(paragraph(emphasis(text('Schema '), inlineCode(name), text(` is shown ${location}.`))));
|
|
152
|
+
return nodes;
|
|
153
|
+
};
|
|
154
|
+
const render = (input, depth = 0, ancestors = [], options = {}) => {
|
|
155
|
+
// Follow the original target: merging reference siblings creates fresh objects.
|
|
156
|
+
const identity = getResolvedRef(input) ?? input;
|
|
157
|
+
if (typeof identity === 'object' && ancestors.includes(identity)) {
|
|
158
|
+
return [paragraph(emphasis(text('[Circular Reference]')))];
|
|
159
|
+
}
|
|
160
|
+
const value = view(input);
|
|
161
|
+
// A named model with structural reference siblings is its own schema, not another
|
|
162
|
+
// occurrence of its target. Otherwise either rendering order can hide properties.
|
|
163
|
+
const sharedIdentity = isObject(input) && '$ref' in input && Object.keys(input).some((key) => structuralKeywords.has(key))
|
|
164
|
+
? input
|
|
165
|
+
: identity;
|
|
166
|
+
const shared = typeof sharedIdentity === 'object' && sharedIdentity !== null ? sharedIdentity : undefined;
|
|
167
|
+
const name = options.name ?? value.name;
|
|
168
|
+
if (shared && name !== undefined) {
|
|
169
|
+
// Expanding every path through a shared schema grows exponentially, so expand it once.
|
|
170
|
+
const previous = shown.get(shared);
|
|
171
|
+
if (previous !== undefined) {
|
|
172
|
+
// A model section already prints its own annotations above the schema.
|
|
173
|
+
const referenceOptions = options.name === undefined ? options : { ...options, hideDetails: true };
|
|
174
|
+
return reference(input, value, referenceOptions, previous, 'above');
|
|
175
|
+
}
|
|
176
|
+
if (value.name !== undefined && options.name === undefined && depth >= MAX_DEPTH && sections.has(shared))
|
|
177
|
+
return reference(input, value, options, value.name, 'below under Schemas');
|
|
178
|
+
}
|
|
179
|
+
if (depth >= MAX_DEPTH)
|
|
180
|
+
return [paragraph(text('[Maximum schema depth reached]'))];
|
|
181
|
+
if (nodeCount >= maxNodes)
|
|
182
|
+
return [paragraph(emphasis(text('[Schema output truncated]')))];
|
|
183
|
+
nodeCount++;
|
|
184
|
+
if (shared && name !== undefined && !shown.has(shared))
|
|
185
|
+
shown.set(shared, name);
|
|
186
|
+
if (typeof value.schema === 'boolean')
|
|
187
|
+
return options.hideDetails ? [] : [paragraph(...details(value))];
|
|
188
|
+
const childAncestors = [...ancestors, identity];
|
|
189
|
+
const nodes = [];
|
|
190
|
+
for (const [key, label] of [
|
|
191
|
+
['allOf', 'All of:'],
|
|
192
|
+
['anyOf', 'Any of:'],
|
|
193
|
+
['oneOf', 'One of:'],
|
|
194
|
+
]) {
|
|
195
|
+
if (value[key]?.length)
|
|
196
|
+
nodes.push(paragraph(strong(text(label))), ...value[key].flatMap((child) => render(child, depth + 1, childAncestors)));
|
|
197
|
+
}
|
|
198
|
+
if (value.not !== undefined)
|
|
199
|
+
nodes.push(paragraph(strong(text('Not:'))), ...render(value.not, depth + 1, childAncestors));
|
|
200
|
+
const array = value.type === 'array' || value.items !== undefined;
|
|
201
|
+
if (!options.hideDetails) {
|
|
202
|
+
// Child sections imply a single container type, but never its nullable alternatives.
|
|
203
|
+
const impliedType = (value.type === 'object' && value.properties.length > 0) ||
|
|
204
|
+
(value.type === 'array' && value.items !== undefined);
|
|
205
|
+
const annotations = details(value, false, options.hideDescription, !impliedType);
|
|
206
|
+
if (annotations.length)
|
|
207
|
+
nodes.push(paragraph(...annotations));
|
|
208
|
+
}
|
|
209
|
+
if (value.properties.length) {
|
|
210
|
+
const properties = value.properties.map(([name, schema]) => {
|
|
211
|
+
const child = view(schema);
|
|
212
|
+
const label = [inlineCode(name)];
|
|
213
|
+
if (value.required.has(name))
|
|
214
|
+
label.push(text(' (required)'));
|
|
215
|
+
const blocks = [paragraph(strong(...label)), paragraph(...details(child, true))];
|
|
216
|
+
blocks.push(...render(schema, depth + 1, childAncestors, {
|
|
217
|
+
hideDetails: true,
|
|
218
|
+
property: true,
|
|
219
|
+
}));
|
|
220
|
+
return item(...blocks);
|
|
221
|
+
});
|
|
222
|
+
nodes.push(list(properties));
|
|
223
|
+
}
|
|
224
|
+
if (array && value.items !== undefined) {
|
|
225
|
+
nodes.push(paragraph(strong(text(options.property ? 'Items:' : 'Array of:'))), ...render(value.items, depth + 1, childAncestors));
|
|
226
|
+
}
|
|
227
|
+
const constraints = [];
|
|
228
|
+
if (value.minItems !== undefined)
|
|
229
|
+
constraints.push(item(paragraph(text('Min items: '), inlineCode(value.minItems))));
|
|
230
|
+
if (value.maxItems !== undefined)
|
|
231
|
+
constraints.push(item(paragraph(text('Max items: '), inlineCode(value.maxItems))));
|
|
232
|
+
if (value.uniqueItems !== undefined)
|
|
233
|
+
constraints.push(item(paragraph(text('Unique items: '), inlineCode(value.uniqueItems))));
|
|
234
|
+
if (constraints.length)
|
|
235
|
+
nodes.push(list(constraints));
|
|
236
|
+
if (value.additionalProperties !== undefined)
|
|
237
|
+
nodes.push(paragraph(strong(text('Additional properties:'))), ...render(value.additionalProperties, depth + 1, childAncestors));
|
|
238
|
+
if (value.discriminator) {
|
|
239
|
+
nodes.push(paragraph(strong(text('Discriminator:')), text(' '), inlineCode(value.discriminator.propertyName)));
|
|
240
|
+
const mappings = Object.entries(value.discriminator.mapping ?? {});
|
|
241
|
+
if (mappings.length)
|
|
242
|
+
nodes.push(list(mappings.map(([name, target]) => item(paragraph(inlineCode(name), text(': '), inlineCode(target))))));
|
|
243
|
+
}
|
|
244
|
+
return nodes;
|
|
245
|
+
};
|
|
246
|
+
return {
|
|
247
|
+
view,
|
|
248
|
+
render,
|
|
249
|
+
beginSection: () => {
|
|
250
|
+
nodeCount = 0;
|
|
251
|
+
},
|
|
252
|
+
forDocument,
|
|
253
|
+
};
|
|
146
254
|
};
|
|
147
|
-
return
|
|
255
|
+
return forDocument();
|
|
148
256
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { OperationMethod } from '@scalar/workspace-store/schemas/navigation';
|
|
2
2
|
import type { OpenApiDocument } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
|
|
3
3
|
/** Identify one operation by path and method, operation ID, or JSON pointer. */
|
|
4
4
|
export type OperationSelector = {
|
|
5
5
|
path: string;
|
|
6
|
-
method:
|
|
6
|
+
method: OperationMethod;
|
|
7
7
|
} | {
|
|
8
8
|
operationId: string;
|
|
9
9
|
} | {
|
|
@@ -19,7 +19,7 @@ type PageSelectors = {
|
|
|
19
19
|
model: string;
|
|
20
20
|
webhook: {
|
|
21
21
|
name: string;
|
|
22
|
-
method:
|
|
22
|
+
method: OperationMethod;
|
|
23
23
|
};
|
|
24
24
|
introduction: true;
|
|
25
25
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select-document.d.ts","sourceRoot":"","sources":["../src/select-document.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"select-document.d.ts","sourceRoot":"","sources":["../src/select-document.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAA;AACjF,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,8DAA8D,CAAA;AAEnH,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GACzB;IACE,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,eAAe,CAAA;CACxB,GACD;IACE,WAAW,EAAE,MAAM,CAAA;CACpB,GACD;IACE,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AACL,2EAA2E;AAC3E,MAAM,MAAM,oBAAoB,GAC5B;KACG,GAAG,IAAI,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,aAAa,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,aAAa,EAAE,GAAG,CAAC;CACnH,CAAC,MAAM,aAAa,CAAC,GACtB,OAAO,CAAC,MAAM,CAAC,MAAM,aAAa,EAAE,KAAK,CAAC,CAAC,CAAA;AAE/C,KAAK,aAAa,GAAG;IACnB,SAAS,EAAE,iBAAiB,CAAA;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,eAAe,CAAA;KAAE,CAAA;IAClD,YAAY,EAAE,IAAI,CAAA;CACnB,CAAA;AAyKD,sEAAsE;AACtE,eAAO,MAAM,cAAc,GAAI,UAAU,eAAe,EAAE,UAAS,oBAAyB,KAAG,eAuN9F,CAAA"}
|
package/dist/select-document.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HTTP_METHODS } from '@scalar/helpers/http/http-methods';
|
|
2
2
|
import { isObject } from '@scalar/helpers/object/is-object';
|
|
3
|
-
import { getPathItemOperation, getResolvedPathItem } from '@scalar/workspace-store/helpers/for-each-path-item-operation';
|
|
3
|
+
import { forEachPathItemOperation, getPathItemOperation, getResolvedPathItem, setPathItemOperation, } from '@scalar/workspace-store/helpers/for-each-path-item-operation';
|
|
4
4
|
import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
5
5
|
const HTTP_METHOD_SET = new Set(HTTP_METHODS);
|
|
6
6
|
const normalizeHttpMethod = (method) => {
|
|
@@ -28,17 +28,18 @@ const parseJsonPointer = (pointer) => normalizeJsonPointer(pointer)
|
|
|
28
28
|
.map((segment) => segment.replaceAll('~1', '/').replaceAll('~0', '~'));
|
|
29
29
|
const getOperationSelectorFromPointer = (pointer) => {
|
|
30
30
|
const segments = parseJsonPointer(pointer);
|
|
31
|
-
if (segments
|
|
31
|
+
if (segments[0] !== 'paths' ||
|
|
32
|
+
!(segments.length === 3 || (segments.length === 4 && segments[2] === 'additionalOperations'))) {
|
|
32
33
|
throw new Error(`JSON pointer "${pointer}" must target an operation object under "/paths/{path}/{method}"`);
|
|
33
34
|
}
|
|
34
35
|
const path = segments[1];
|
|
35
|
-
const method = segments[2];
|
|
36
|
-
if (!path || !method) {
|
|
36
|
+
const method = segments.length === 4 ? segments[3] : segments[2];
|
|
37
|
+
if (!path || !method || (segments.length === 3 ? !HTTP_METHOD_SET.has(method) : HTTP_METHOD_SET.has(method))) {
|
|
37
38
|
throw new Error(`JSON pointer "${pointer}" must target an operation object under "/paths/{path}/{method}"`);
|
|
38
39
|
}
|
|
39
40
|
return {
|
|
40
41
|
path,
|
|
41
|
-
method
|
|
42
|
+
method,
|
|
42
43
|
};
|
|
43
44
|
};
|
|
44
45
|
const getPathEntries = (document) => {
|
|
@@ -51,12 +52,20 @@ const getPathEntries = (document) => {
|
|
|
51
52
|
return pathItem ? [[path, pathItem]] : [];
|
|
52
53
|
});
|
|
53
54
|
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
/** Keep path metadata while excluding every unselected fixed or additional operation. */
|
|
56
|
+
const filterPathItemOperations = (pathItem, methods) => {
|
|
57
|
+
const selected = Object.fromEntries(Object.entries(pathItem).filter(([key]) => !HTTP_METHOD_SET.has(key) && key !== 'additionalOperations'));
|
|
58
|
+
forEachPathItemOperation(pathItem, (method, operation) => {
|
|
59
|
+
if (methods.includes(method)) {
|
|
60
|
+
setPathItemOperation(selected, method, operation);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return selected;
|
|
64
|
+
};
|
|
65
|
+
/** Exact authored methods take precedence over the legacy uppercase fixed-method aliases. */
|
|
66
|
+
const resolveMethod = (pathItem, method) => getPathItemOperation(pathItem, method) ? method : normalizeHttpMethod(method);
|
|
58
67
|
const findOperationByPathAndMethod = (document, selector) => {
|
|
59
|
-
const method =
|
|
68
|
+
const method = resolveMethod(getResolvedPathItem(document.paths?.[selector.path]), selector.method);
|
|
60
69
|
if (!method) {
|
|
61
70
|
throw new Error(`Invalid HTTP method "${selector.method}". Supported methods: ${HTTP_METHODS.join(', ')}`);
|
|
62
71
|
}
|
|
@@ -69,20 +78,22 @@ const findOperationByPathAndMethod = (document, selector) => {
|
|
|
69
78
|
method,
|
|
70
79
|
};
|
|
71
80
|
};
|
|
72
|
-
const findOperationsByOperationId = (document, operationId) => getPathEntries(document).flatMap(([path, pathItem]) =>
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return [{ path, method }];
|
|
82
|
-
}));
|
|
81
|
+
const findOperationsByOperationId = (document, operationId) => getPathEntries(document).flatMap(([path, pathItem]) => {
|
|
82
|
+
const matches = [];
|
|
83
|
+
forEachPathItemOperation(pathItem, (method, operation) => {
|
|
84
|
+
if (getResolvedRef(operation)?.operationId === operationId) {
|
|
85
|
+
matches.push({ path, method });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return matches;
|
|
89
|
+
});
|
|
83
90
|
const resolveOperationMatch = (document, selector) => {
|
|
84
91
|
if ('pointer' in selector) {
|
|
85
|
-
|
|
92
|
+
const match = getOperationSelectorFromPointer(selector.pointer);
|
|
93
|
+
if (!getPathItemOperation(document.paths?.[match.path], match.method)) {
|
|
94
|
+
throw new Error(`Operation not found at JSON pointer "${selector.pointer}"`);
|
|
95
|
+
}
|
|
96
|
+
return match;
|
|
86
97
|
}
|
|
87
98
|
if ('operationId' in selector) {
|
|
88
99
|
const matches = findOperationsByOperationId(document, selector.operationId);
|
|
@@ -106,7 +117,7 @@ const filterDocumentByOperation = (document, selector) => {
|
|
|
106
117
|
return {
|
|
107
118
|
...document,
|
|
108
119
|
paths: {
|
|
109
|
-
[match.path]:
|
|
120
|
+
[match.path]: filterPathItemOperations(pathItem, [match.method]),
|
|
110
121
|
},
|
|
111
122
|
};
|
|
112
123
|
};
|
|
@@ -157,9 +168,14 @@ export const selectDocument = (document, options = {}) => {
|
|
|
157
168
|
}
|
|
158
169
|
selected.tags = metadata.length ? metadata : [{ name: options.tag }];
|
|
159
170
|
for (const [path, item] of getPathEntries(document)) {
|
|
160
|
-
const methods =
|
|
171
|
+
const methods = [];
|
|
172
|
+
forEachPathItemOperation(item, (method, operation) => {
|
|
173
|
+
if (getResolvedRef(operation)?.tags?.includes(options.tag)) {
|
|
174
|
+
methods.push(method);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
161
177
|
if (methods.length) {
|
|
162
|
-
selected.paths[path] =
|
|
178
|
+
selected.paths[path] = filterPathItemOperations(item, methods);
|
|
163
179
|
}
|
|
164
180
|
}
|
|
165
181
|
if (!metadata.length && !Object.keys(selected.paths ?? {}).length) {
|
|
@@ -182,26 +198,31 @@ export const selectDocument = (document, options = {}) => {
|
|
|
182
198
|
Object.keys(selector).length !== 2) {
|
|
183
199
|
throw new Error('Invalid webhook selector. Use { name, method }');
|
|
184
200
|
}
|
|
185
|
-
const
|
|
201
|
+
const item = getResolvedPathItem(document.webhooks?.[selector.name]);
|
|
202
|
+
const method = resolveMethod(item, selector.method);
|
|
186
203
|
if (!method) {
|
|
187
204
|
throw new Error(`Invalid HTTP method "${selector.method}"`);
|
|
188
205
|
}
|
|
189
|
-
const item = getResolvedPathItem(document.webhooks?.[selector.name]);
|
|
190
206
|
if (!item || !getPathItemOperation(item, method)) {
|
|
191
207
|
throw new Error(`Webhook "${selector.name}" with method "${method.toUpperCase()}" was not found`);
|
|
192
208
|
}
|
|
193
|
-
selected.webhooks = { [selector.name]:
|
|
209
|
+
selected.webhooks = { [selector.name]: filterPathItemOperations(item, [method]) };
|
|
194
210
|
}
|
|
195
211
|
const securityNames = new Set();
|
|
196
212
|
const tagNames = new Set();
|
|
197
213
|
for (const items of [selected.paths, selected.webhooks]) {
|
|
198
214
|
for (const [path, itemRef] of Object.entries(items ?? {})) {
|
|
199
215
|
const item = getResolvedPathItem(itemRef);
|
|
200
|
-
const scoped = {
|
|
201
|
-
|
|
202
|
-
|
|
216
|
+
const scoped = {
|
|
217
|
+
...item,
|
|
218
|
+
additionalOperations: item.additionalOperations ? { ...item.additionalOperations } : undefined,
|
|
219
|
+
parameters: undefined,
|
|
220
|
+
servers: undefined,
|
|
221
|
+
};
|
|
222
|
+
forEachPathItemOperation(item, (method, operationRef) => {
|
|
223
|
+
const operation = getResolvedRef(operationRef);
|
|
203
224
|
if (!operation) {
|
|
204
|
-
|
|
225
|
+
return;
|
|
205
226
|
}
|
|
206
227
|
const parameters = new Map();
|
|
207
228
|
for (const ref of [...(item.parameters ?? []), ...(operation.parameters ?? [])]) {
|
|
@@ -219,14 +240,14 @@ export const selectDocument = (document, options = {}) => {
|
|
|
219
240
|
for (const name of operation.tags ?? []) {
|
|
220
241
|
tagNames.add(name);
|
|
221
242
|
}
|
|
222
|
-
scoped
|
|
243
|
+
setPathItemOperation(scoped, method, {
|
|
223
244
|
...operation,
|
|
224
245
|
parameters: [...parameters.values()],
|
|
225
246
|
servers: operation.servers ?? item.servers ?? document.servers,
|
|
226
247
|
security,
|
|
227
248
|
tags: options.tag !== undefined ? [options.tag] : operation.tags,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
230
251
|
items[path] = scoped;
|
|
231
252
|
}
|
|
232
253
|
}
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"llm",
|
|
17
17
|
"swagger"
|
|
18
18
|
],
|
|
19
|
-
"version": "1.
|
|
19
|
+
"version": "1.2.0",
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=22"
|
|
22
22
|
},
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@scalar/code-highlight": "0.4.7",
|
|
44
|
-
"@scalar/helpers": "0.
|
|
45
|
-
"@scalar/json-magic": "0.15.
|
|
46
|
-
"@scalar/openapi-upgrader": "0.3.
|
|
47
|
-
"@scalar/workspace-store": "0.
|
|
44
|
+
"@scalar/helpers": "0.14.0",
|
|
45
|
+
"@scalar/json-magic": "0.15.1",
|
|
46
|
+
"@scalar/openapi-upgrader": "0.3.1",
|
|
47
|
+
"@scalar/workspace-store": "0.66.0",
|
|
48
48
|
"rehype-parse": "^9.0.1",
|
|
49
49
|
"rehype-remark": "^10.0.1",
|
|
50
50
|
"rehype-sanitize": "^6.0.0",
|