Generate OpenAPI field descriptions and validate generated clients in CI
This commit is contained in:
1 parent
9eef6f22c9
commit
6156387251
8 files changed
+4329
-38512
No files matched your search
@@ -23,6 +23,7 @@ jobs:
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
- run: pnpm install
|
||||
- run: pnpm openapi:generate
|
||||
- run: pnpm openapi:check --explicit
|
||||
- run: pnpm check:api-client
|
||||
- run: pnpm build-packages
|
||||
|
||||
@@ -21,6 +21,7 @@ next-env.d.ts
|
||||
/build
|
||||
/public/script.js
|
||||
/public/recorder.js
|
||||
/public/openapi.json
|
||||
/geo
|
||||
/dist
|
||||
/packages/*/dist
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
-38242
File diff suppressed because it is too large.
Load diff
+13
-2
@@ -4,6 +4,9 @@ Umami generates `public/openapi.json` for every App Router API handler. It stati
|
||||
baseline contract from route source and replaces that baseline with a colocated, Zod-backed
|
||||
contract wherever one exists.
|
||||
|
||||
`public/openapi.json` is an ignored build artifact and must not be committed. Generate it locally
|
||||
with `pnpm openapi:generate` before running checks or generating the API client.
|
||||
|
||||
## Commands
|
||||
|
||||
- `pnpm openapi:generate` regenerates the public OpenAPI document.
|
||||
@@ -14,8 +17,8 @@ contract wherever one exists.
|
||||
contract.
|
||||
- `pnpm openapi:check --verbose` lists operations that still use inferred contracts.
|
||||
|
||||
The normal and Docker builds run the generator before the Next.js build. CI runs the check before
|
||||
tests and build steps.
|
||||
The normal and Docker builds run the generator before the Next.js build. CI generates the artifact
|
||||
and runs the check before tests and build steps.
|
||||
|
||||
## Adding an operation
|
||||
|
||||
@@ -68,6 +71,14 @@ Run `pnpm generate:api` after editing to refresh the OpenAPI document and API cl
|
||||
`pnpm openapi:check` reject entries that do not match a discovered route and method. Entries are
|
||||
optional, so documentation can be improved incrementally.
|
||||
|
||||
Field documentation lives in `src/openapi/field-descriptions.ts`. The document builder fills missing
|
||||
query/path parameter and request/response property descriptions after converting contracts to
|
||||
OpenAPI, including nested schemas and shared components. Existing contract descriptions take
|
||||
precedence. Keep shared wording limited to fields with consistent meanings; use endpoint-specific
|
||||
wording for fields such as the dashboard's `parameters`. Unknown fields are left undocumented
|
||||
rather than given an automatically generated label. Examples and schema validation rules are not
|
||||
modified. Run `pnpm generate:api` after editing this file as well.
|
||||
|
||||
## Operation IDs
|
||||
|
||||
Operation IDs are public: they become method names in `@umami/api-client`. Routes with a curated
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getCoverageErrors,
|
||||
} from '@/openapi/coverage';
|
||||
import { discoverApiOperations } from '@/openapi/discover';
|
||||
import { applyFieldDescriptions } from '@/openapi/field-descriptions';
|
||||
import { inferApiContracts } from '@/openapi/infer';
|
||||
import { type ApiAudience, type ApiOperationContract, getOperationKey } from '@/openapi/operation';
|
||||
import { operationDescriptions } from '@/openapi/operation-descriptions';
|
||||
@@ -147,5 +148,7 @@ export async function buildOpenApiDocument(
|
||||
},
|
||||
);
|
||||
|
||||
applyFieldDescriptions(document);
|
||||
|
||||
return { document, coverage };
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { createDocument } from 'zod-openapi';
|
||||
import { applyFieldDescriptions } from './field-descriptions';
|
||||
|
||||
function documentWithSchema(schema: Record<string, unknown>) {
|
||||
return {
|
||||
openapi: '3.1.0',
|
||||
info: { title: 'Test', version: '1' },
|
||||
paths: {
|
||||
'/api/dashboard': {
|
||||
get: {
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Success',
|
||||
content: { 'application/json': { schema } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies ReturnType<typeof createDocument>;
|
||||
}
|
||||
|
||||
describe('OpenAPI field descriptions', () => {
|
||||
it('documents dashboard fields without changing their schemas or example payloads', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string', description: 'Custom dashboard name.' },
|
||||
parameters: { anyOf: [{ type: 'object' }, { type: 'null' }] },
|
||||
createdAt: { type: 'string', format: 'date-time' },
|
||||
},
|
||||
required: ['id', 'name', 'parameters', 'createdAt'],
|
||||
example: { properties: { id: { type: 'string' } } },
|
||||
};
|
||||
const original = structuredClone(schema);
|
||||
const document = documentWithSchema(schema);
|
||||
applyFieldDescriptions(document);
|
||||
|
||||
expect(schema).toEqual({
|
||||
...original,
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Dashboard board ID, equal to the owning user ID.' },
|
||||
name: original.properties.name,
|
||||
parameters: {
|
||||
...original.properties.parameters,
|
||||
description: 'Dashboard configuration, including components, layout, and saved reports.',
|
||||
},
|
||||
createdAt: {
|
||||
...original.properties.createdAt,
|
||||
description: 'Date and time the record was created.',
|
||||
},
|
||||
},
|
||||
});
|
||||
const once = structuredClone(document);
|
||||
applyFieldDescriptions(document);
|
||||
expect(document).toEqual(once);
|
||||
});
|
||||
|
||||
it('traverses nested schemas and components while preserving references and unknown fields', () => {
|
||||
const nested = {
|
||||
properties: {
|
||||
userId: { type: 'string' },
|
||||
unknownField: { type: 'string' },
|
||||
},
|
||||
};
|
||||
const reference = { $ref: '#/components/schemas/User' };
|
||||
const schema = {
|
||||
allOf: [
|
||||
{ type: 'array', items: { oneOf: [nested, reference] } },
|
||||
{ additionalProperties: { anyOf: [structuredClone(nested)] } },
|
||||
],
|
||||
};
|
||||
const document = {
|
||||
...documentWithSchema(schema),
|
||||
components: { schemas: { User: { properties: { username: { type: 'string' as const } } } } },
|
||||
};
|
||||
applyFieldDescriptions(document);
|
||||
expect(nested.properties.userId).toHaveProperty('description');
|
||||
expect(nested.properties.unknownField).not.toHaveProperty('description');
|
||||
expect(reference).toEqual({ $ref: '#/components/schemas/User' });
|
||||
expect(document.components.schemas.User.properties.username).toHaveProperty('description');
|
||||
expect(schema.allOf[1].additionalProperties.anyOf[0].properties.userId).toHaveProperty(
|
||||
'description',
|
||||
);
|
||||
});
|
||||
|
||||
it('fills query and path descriptions while respecting curated parameter and schema wording', () => {
|
||||
const parameters = [
|
||||
{ name: 'startAt', in: 'query' as const, schema: { type: 'number' as const } },
|
||||
{ name: 'boardId', in: 'path' as const, required: true, schema: { type: 'string' as const } },
|
||||
{
|
||||
name: 'type',
|
||||
in: 'query' as const,
|
||||
schema: { type: 'string' as const, description: 'Dimension to rank.' },
|
||||
},
|
||||
{ name: 'page', in: 'query' as const, description: 'Curated page description.' },
|
||||
];
|
||||
const document = documentWithSchema({});
|
||||
Object.assign(document.paths['/api/dashboard'].get, { parameters });
|
||||
applyFieldDescriptions(document);
|
||||
expect(parameters[0]).toHaveProperty(
|
||||
'description',
|
||||
'Start of the date range as a Unix timestamp in milliseconds.',
|
||||
);
|
||||
expect(parameters[1]).toHaveProperty('description', 'ID of the board.');
|
||||
expect(parameters[2]).toHaveProperty('description', 'Dimension to rank.');
|
||||
expect(parameters[3]).toHaveProperty('description', 'Curated page description.');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,304 @@
|
||||
import type { createDocument } from 'zod-openapi';
|
||||
|
||||
// Shared wording only belongs here when a field has the same meaning across endpoints.
|
||||
// More specific descriptions in contracts always take precedence.
|
||||
const fieldDescriptions: Record<string, string> = {
|
||||
id: 'Unique identifier of the resource.',
|
||||
name: 'Display name of the resource.',
|
||||
description: 'Description of the resource.',
|
||||
parameters: 'Configuration parameters for the resource.',
|
||||
createdAt: 'Date and time the record was created.',
|
||||
updatedAt: 'Date and time the record was last updated.',
|
||||
deletedAt: 'Date and time the record was deleted, if applicable.',
|
||||
resetAt: 'Date and time the website analytics were last reset.',
|
||||
createdBy: 'ID of the user who created the resource.',
|
||||
userId: 'ID of the associated user.',
|
||||
teamId: 'ID of the associated team.',
|
||||
websiteId: 'ID of the website.',
|
||||
boardId: 'ID of the board.',
|
||||
linkId: 'ID of the tracked link.',
|
||||
pixelId: 'ID of the tracking pixel.',
|
||||
reportId: 'ID of the saved report.',
|
||||
sessionId: 'ID of the visitor session.',
|
||||
eventId: 'ID of the event.',
|
||||
websiteEventId: 'ID of the website event.',
|
||||
annotationId: 'ID of the annotation.',
|
||||
segmentId: 'ID of the saved segment or cohort.',
|
||||
replayId: 'ID of the session replay.',
|
||||
shareId: 'Identifier used to access a shared resource.',
|
||||
keyId: 'ID of the API key.',
|
||||
visitId: 'ID of the visit.',
|
||||
entityId: 'ID of the resource being shared.',
|
||||
slug: 'URL slug used to access the resource.',
|
||||
username: 'Username of the account.',
|
||||
user: 'User associated with the resource.',
|
||||
team: 'Team associated with the resource.',
|
||||
teams: 'Teams associated with the user.',
|
||||
members: 'Members of the team.',
|
||||
createUser: 'User who created the resource.',
|
||||
boards: 'Boards associated with the user.',
|
||||
links: 'Tracked links associated with the user.',
|
||||
pixels: 'Tracking pixels associated with the user.',
|
||||
reports: 'Saved reports associated with the user.',
|
||||
apiKeys: 'API keys associated with the user.',
|
||||
password: 'Password used to authenticate the account.',
|
||||
currentPassword: 'Current account password.',
|
||||
newPassword: 'New password to set for the account.',
|
||||
isAdmin: 'Whether the user has administrator privileges.',
|
||||
role: 'Permission role assigned to the user.',
|
||||
accessCode: 'Code used to join the team.',
|
||||
logoUrl: 'URL of the logo image.',
|
||||
domain: 'Domain name associated with the resource.',
|
||||
url: 'URL associated with the resource.',
|
||||
page: 'Page number, starting at 1.',
|
||||
pageSize: 'Number of results per page.',
|
||||
maxResults: 'Maximum number of results to include.',
|
||||
limit: 'Maximum number of rows to return.',
|
||||
offset: 'Number of rows to skip before returning results.',
|
||||
orderBy: 'Field to sort the results by.',
|
||||
sortDescending: 'Whether to sort results in descending order.',
|
||||
search: 'Search text used to filter results.',
|
||||
isCapped: 'Whether the results were truncated by the maximum result limit.',
|
||||
data: 'Data returned by the operation.',
|
||||
count: 'Number of matching records.',
|
||||
startAt: 'Start of the date range as a Unix timestamp in milliseconds.',
|
||||
endAt: 'End of the date range as a Unix timestamp in milliseconds.',
|
||||
startDate: 'Start of the date range as an ISO 8601 date or date-time.',
|
||||
endDate: 'End of the date range as an ISO 8601 date or date-time.',
|
||||
timezone:
|
||||
'IANA time zone used to interpret dates and group results, for example America/New_York.',
|
||||
unit: 'Time interval used to group results: minute, hour, day, month, or year.',
|
||||
filters: 'Filters applied to the analytics data.',
|
||||
match: 'Whether records must match all filters or any filter.',
|
||||
segment: 'ID of a saved segment used to filter results.',
|
||||
cohort: 'ID of a saved cohort used to filter visitors.',
|
||||
excludeBounce: 'Set a non-empty value to exclude visits with only one pageview.',
|
||||
hostname: 'Hostname on which the activity occurred.',
|
||||
browser: 'Browser used by the visitor.',
|
||||
os: 'Operating system used by the visitor.',
|
||||
device: 'Device category used by the visitor.',
|
||||
screen: 'Screen resolution of the visitor device.',
|
||||
language: 'Preferred language reported by the visitor browser.',
|
||||
country: 'Country code of the visitor.',
|
||||
region: 'Region or subdivision of the visitor.',
|
||||
city: 'City of the visitor.',
|
||||
distinctId: 'Custom identifier assigned to the visitor.',
|
||||
urlPath: 'Path portion of the page URL.',
|
||||
urlQuery: 'Query string portion of the page URL.',
|
||||
referrerDomain: 'Domain of the referring page.',
|
||||
referrer: 'URL of the referring page.',
|
||||
title: 'Title of the page.',
|
||||
pageTitle: 'Title of the page.',
|
||||
eventName: 'Name of the custom event.',
|
||||
eventType: 'Event type: 1 for a pageview or 2 for a custom event.',
|
||||
tag: 'Tag attached to the tracked activity.',
|
||||
utmSource: 'UTM campaign source.',
|
||||
utmMedium: 'UTM campaign medium.',
|
||||
utmCampaign: 'UTM campaign name.',
|
||||
utmContent: 'UTM campaign content.',
|
||||
utmTerm: 'UTM campaign search term.',
|
||||
pageviews: 'Pageview counts for the selected period.',
|
||||
visitors: 'Unique visitor counts for the selected period.',
|
||||
visits: 'Visit counts for the selected period.',
|
||||
views: 'Pageview counts for the selected period.',
|
||||
bounces: 'Number of visits with only one pageview.',
|
||||
totaltime: 'Total visit duration in seconds.',
|
||||
firstAt: 'Date and time of the first recorded activity.',
|
||||
lastAt: 'Date and time of the most recent recorded activity.',
|
||||
comparison: 'Analytics for the comparison period.',
|
||||
uniqueEvents: 'Number of unique events.',
|
||||
average: 'Arithmetic mean of the numeric property values.',
|
||||
median: 'Median of the numeric property values.',
|
||||
min: 'Minimum numeric property value.',
|
||||
max: 'Maximum numeric property value.',
|
||||
currency: 'Currency code used for revenue values.',
|
||||
propertyName: 'Name of the custom event or session property.',
|
||||
propertyValue: 'Value of the custom property.',
|
||||
propertyKeys: 'Names of the custom properties.',
|
||||
propertyValues: 'Values of the custom properties.',
|
||||
dataKey: 'Name of the custom property.',
|
||||
dataType: 'Data type of the custom property.',
|
||||
stringValue: 'Custom property value stored as a string.',
|
||||
numberValue: 'Custom property value stored as a number.',
|
||||
dateValue: 'Custom property value stored as a date-time.',
|
||||
eventProperties: 'Custom properties recorded with the event.',
|
||||
minDuration: 'Minimum replay duration in seconds.',
|
||||
startedAt: 'Date and time the recording started.',
|
||||
endedAt: 'Date and time the recording ended.',
|
||||
chunkCount: 'Number of stored recording chunks.',
|
||||
eventCount: 'Number of recorded replay events.',
|
||||
isSaved: 'Whether the replay has been saved.',
|
||||
duration: 'Replay duration in milliseconds.',
|
||||
viewportW: 'Viewport width in pixels.',
|
||||
viewportH: 'Viewport height in pixels.',
|
||||
pageW: 'Page width in pixels.',
|
||||
pageH: 'Page height in pixels.',
|
||||
pageX: 'Horizontal position on the page in pixels.',
|
||||
pageY: 'Vertical position on the page in pixels.',
|
||||
recorderEnabled: 'Whether recording is enabled for the website.',
|
||||
replayConfig: 'Session replay and heatmap recording configuration.',
|
||||
replayEnabled: 'Whether session replay recording is enabled.',
|
||||
heatmapEnabled: 'Whether heatmap recording is enabled.',
|
||||
sampleRate: 'Fraction of sessions to record for replay, from 0 to 1.',
|
||||
heatmapSampleRate: 'Fraction of sessions to record for heatmaps, from 0 to 1.',
|
||||
maskLevel: 'Privacy masking level used by the recorder: strict or moderate.',
|
||||
blockSelector: 'CSS selector for elements excluded from recordings.',
|
||||
allDay: 'Whether the annotation applies to the entire day.',
|
||||
note: 'Text of the annotation.',
|
||||
apiKey: 'API key authentication details.',
|
||||
keyPrefix: 'Visible prefix used to identify an API key.',
|
||||
lastUsedAt: 'Date and time the credential was last used.',
|
||||
expiresAt: 'Date and time the credential expires.',
|
||||
revokedAt: 'Date and time the credential was revoked.',
|
||||
usedAt: 'Date and time the code was used.',
|
||||
backupCode: 'Single-use backup code for two-factor authentication.',
|
||||
backupCodes: 'Single-use backup codes for two-factor authentication.',
|
||||
twoFactorRequired: 'Whether two-factor authentication is required.',
|
||||
requiresTwoFactor: 'Whether login requires a second authentication factor.',
|
||||
isEnabled: 'Whether two-factor authentication is enabled.',
|
||||
isConfigured: 'Whether two-factor authentication has been configured.',
|
||||
isRequired: 'Whether two-factor authentication is required for this user.',
|
||||
globalRequired: 'Whether two-factor authentication is required for all users.',
|
||||
requiredReason: 'Reason two-factor authentication is required.',
|
||||
partialToken: 'Temporary login token used to complete two-factor authentication.',
|
||||
manualKey: 'Secret key for manually configuring an authenticator app.',
|
||||
qrCodeDataUrl: 'Data URL of the QR code for authenticator setup.',
|
||||
lockedUntil: 'Time until which further authentication attempts are blocked.',
|
||||
error: 'Error details returned when the operation fails.',
|
||||
message: 'Human-readable explanation of the result.',
|
||||
ok: 'Whether the operation succeeded.',
|
||||
client_id: 'OAuth client identifier.',
|
||||
client_name: 'Human-readable name of the OAuth client.',
|
||||
client_uri: 'URL of the OAuth client homepage.',
|
||||
logo_uri: 'URL of the OAuth client logo.',
|
||||
redirect_uri: 'Registered redirect URI to receive the authorization response.',
|
||||
redirect_uris: 'Redirect URIs registered for the OAuth client.',
|
||||
response_type: 'OAuth authorization response type; use code.',
|
||||
response_types: 'Authorization response types supported by the client.',
|
||||
code_challenge: 'PKCE challenge derived from the code verifier.',
|
||||
code_challenge_method: 'Method used to derive the PKCE challenge; use S256.',
|
||||
code_verifier: 'Original PKCE verifier used to obtain the authorization code.',
|
||||
scope: 'Space-separated OAuth permissions.',
|
||||
resource: 'Resource URI identifying the API targeted by the OAuth request.',
|
||||
state: 'Client-provided value returned unchanged to correlate the authorization response.',
|
||||
access_token: 'OAuth access token used to authorize API requests.',
|
||||
refresh_token: 'OAuth refresh token used to obtain a new access token.',
|
||||
token_type: 'Authorization scheme for the access token.',
|
||||
expires_in: 'Access token lifetime in seconds.',
|
||||
grant_type: 'OAuth grant used to obtain or refresh an access token.',
|
||||
grant_types: 'OAuth grant types supported by the client.',
|
||||
token_endpoint_auth_method: 'Client authentication method used at the token endpoint.',
|
||||
token_type_hint: 'Hint indicating whether the revoked token is an access or refresh token.',
|
||||
error_description: 'Human-readable explanation of the OAuth error.',
|
||||
shareType: 'Type of resource made available by the share.',
|
||||
};
|
||||
|
||||
const queryDescriptions: Record<string, string> = {
|
||||
compare: 'Comparison period: prev for the previous period or yoy for the same period last year.',
|
||||
ids: 'Comma-separated resource IDs. Provide between 1 and 20 IDs.',
|
||||
includeTeams: 'Set a non-empty value to include websites accessible through team membership.',
|
||||
type: 'Type of resource or analytics dimension to return.',
|
||||
metric: 'Numeric aggregation to calculate for the selected property.',
|
||||
path: 'Filter by page URL path.',
|
||||
referrer: 'Filter by referring URL.',
|
||||
title: 'Filter by page title.',
|
||||
query: 'Filter by page URL query string.',
|
||||
event: 'Filter by custom event name.',
|
||||
};
|
||||
|
||||
const dashboardDescriptions: Record<string, string> = {
|
||||
id: 'Dashboard board ID, equal to the owning user ID.',
|
||||
name: 'Display name of the dashboard.',
|
||||
description: 'Description of the dashboard.',
|
||||
parameters: 'Dashboard configuration, including components, layout, and saved reports.',
|
||||
userId: 'ID of the user who owns the dashboard.',
|
||||
teamId: 'ID of the associated team, if any.',
|
||||
type: 'Board type used for the dashboard.',
|
||||
};
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
|
||||
function isObject(value: unknown): value is JsonObject {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function describeSchema(value: unknown, overrides: Record<string, string> = {}) {
|
||||
if (!isObject(value)) return;
|
||||
|
||||
if (isObject(value.properties)) {
|
||||
for (const [name, property] of Object.entries(value.properties)) {
|
||||
if (!isObject(property)) continue;
|
||||
const description = overrides[name] ?? fieldDescriptions[name];
|
||||
if (!property.description && description) property.description = description;
|
||||
describeSchema(property);
|
||||
}
|
||||
}
|
||||
|
||||
for (const keyword of ['items', 'additionalProperties', 'not', 'if', 'then', 'else']) {
|
||||
describeSchema(value[keyword]);
|
||||
}
|
||||
for (const keyword of ['allOf', 'anyOf', 'oneOf', 'prefixItems']) {
|
||||
const schemas = value[keyword];
|
||||
if (Array.isArray(schemas)) {
|
||||
schemas.forEach(schema => {
|
||||
describeSchema(schema, overrides);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visit only OpenAPI containers and schema keywords, never example/default payloads.
|
||||
function describeContent(value: unknown, overrides: Record<string, string> = {}) {
|
||||
if (!isObject(value)) return;
|
||||
if (isObject(value.content)) {
|
||||
for (const media of Object.values(value.content)) {
|
||||
if (isObject(media)) describeSchema(media.schema, overrides);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function describeParameters(value: unknown) {
|
||||
if (!Array.isArray(value)) return;
|
||||
for (const parameter of value) {
|
||||
if (!isObject(parameter) || typeof parameter.name !== 'string') continue;
|
||||
const schema = isObject(parameter.schema) ? parameter.schema : undefined;
|
||||
const description =
|
||||
schema?.description ??
|
||||
(parameter.in === 'query' ? queryDescriptions[parameter.name] : undefined) ??
|
||||
fieldDescriptions[parameter.name];
|
||||
if (!parameter.description && description) parameter.description = description;
|
||||
describeSchema(schema);
|
||||
describeContent(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
export function applyFieldDescriptions(document: ReturnType<typeof createDocument>) {
|
||||
for (const [path, pathItem] of Object.entries(document.paths ?? {})) {
|
||||
if (!isObject(pathItem)) continue;
|
||||
describeParameters(pathItem.parameters);
|
||||
for (const method of ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']) {
|
||||
const operation = pathItem[method];
|
||||
if (!isObject(operation)) continue;
|
||||
const overrides = path === '/api/dashboard' ? dashboardDescriptions : {};
|
||||
describeParameters(operation.parameters);
|
||||
describeContent(operation.requestBody, overrides);
|
||||
if (isObject(operation.responses)) {
|
||||
Object.values(operation.responses).forEach(response => {
|
||||
describeContent(response, overrides);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const components = document.components;
|
||||
Object.values(components?.schemas ?? {}).forEach(schema => {
|
||||
describeSchema(schema);
|
||||
});
|
||||
Object.values(components?.responses ?? {}).forEach(response => {
|
||||
describeContent(response);
|
||||
});
|
||||
Object.values(components?.requestBodies ?? {}).forEach(body => {
|
||||
describeContent(body);
|
||||
});
|
||||
describeParameters(Object.values(components?.parameters ?? {}));
|
||||
}
|
||||
Reference in new issue
Block a user