mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
### Summary of Changes
Some routes were made visible that aren't actually accessible.
We fix that
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Hide internal `/internal/*` routes from the generated API reference so
docs only show endpoints that are actually accessible. Aligns the docs
with the requirement to hide internal API routes.
- **Bug Fixes**
- Added an explicit filter in `parseOpenAPI` to exclude `/internal`
paths for all audiences.
- Regenerated `docs-mintlify/openapi/{admin,client,server}.json` to
remove internal endpoints.
- No runtime/API changes; docs only.
<sup>Written for commit c7b356a9b1.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1550?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added OAuth authentication endpoints for provider authorization and
token exchange.
* Expanded OAuth provider management with updated schema and additional
configuration options.
* **Bug Fixes**
* Internal endpoints no longer appear in public API documentation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: aman <aman@stack-auth.com>
497 lines
20 KiB
TypeScript
497 lines
20 KiB
TypeScript
import { SmartRouteHandler } from '@/route-handlers/smart-route-handler';
|
|
import { CrudlOperation, EndpointDocumentation } from '@hexclave/shared/dist/crud';
|
|
import { WebhookEvent } from '@hexclave/shared/dist/interface/webhooks';
|
|
import { yupNumber, yupObject, yupString } from '@hexclave/shared/dist/schema-fields';
|
|
import { HexclaveAssertionError, throwErr } from '@hexclave/shared/dist/utils/errors';
|
|
import { HttpMethod } from '@hexclave/shared/dist/utils/http';
|
|
import { typedEntries, typedFromEntries } from '@hexclave/shared/dist/utils/objects';
|
|
import { deindent, stringCompare } from '@hexclave/shared/dist/utils/strings';
|
|
import * as yup from 'yup';
|
|
|
|
function isInternalApiPath(path: string) {
|
|
return path === '/internal' || path.startsWith('/internal/');
|
|
}
|
|
|
|
export function parseOpenAPI(options: {
|
|
endpoints: Map<string, Map<HttpMethod, SmartRouteHandler>>,
|
|
audience: 'client' | 'server' | 'admin',
|
|
}) {
|
|
return {
|
|
openapi: '3.1.0',
|
|
info: {
|
|
title: 'Hexclave REST API',
|
|
version: '1.0.0',
|
|
description: 'The Hexclave REST API. All request headers are documented as canonical `X-Hexclave-*`; the equivalent `X-Stack-*` aliases are accepted on every endpoint for backwards compatibility. Response headers `X-Hexclave-actual-status`, `X-Hexclave-known-error`, and `X-Hexclave-request-id` are emitted alongside their legacy `X-Stack-*` equivalents.',
|
|
},
|
|
servers: [{
|
|
url: 'https://api.hexclave.com/api/v1',
|
|
description: 'Hexclave REST API',
|
|
}],
|
|
paths: Object.fromEntries(
|
|
[...options.endpoints]
|
|
// `/internal/*` routes are scoped to the internal Hexclave project (project.id === "internal")
|
|
// and are not part of the public API. Many of them use a permissive auth.type (e.g. adaptSchema),
|
|
// so the per-audience heuristic below does not exclude them; filter them out explicitly here so
|
|
// they never leak into the public API reference, regardless of their individual route metadata.
|
|
.filter(([path]) => !isInternalApiPath(path))
|
|
.map(([path, handlersByMethod]) => (
|
|
[path, Object.fromEntries(
|
|
[...handlersByMethod]
|
|
.map(([method, handler]) => (
|
|
[method.toLowerCase(), parseRouteHandler({ handler, method, path, audience: options.audience })]
|
|
))
|
|
.filter(([_, handler]) => handler !== undefined)
|
|
)]
|
|
))
|
|
.filter(([_, handlersByMethod]) => Object.keys(handlersByMethod).length > 0)
|
|
.sort(([pathA, handlersByMethodA], [pathB, handlersByMethodB]) => {
|
|
const tagComparison = stringCompare((Object.values(handlersByMethodA)[0] as any).tags[0] ?? "", (Object.values(handlersByMethodB)[0] as any).tags[0] ?? "");
|
|
if (tagComparison !== 0) {
|
|
return tagComparison;
|
|
}
|
|
return stringCompare(pathA, pathB);
|
|
}),
|
|
),
|
|
};
|
|
}
|
|
|
|
export function parseWebhookOpenAPI(options: {
|
|
webhooks: readonly WebhookEvent<any>[],
|
|
}) {
|
|
return {
|
|
openapi: '3.1.0',
|
|
info: {
|
|
title: 'Hexclave Webhooks API',
|
|
version: '1.0.0',
|
|
},
|
|
webhooks: options.webhooks.reduce((acc, webhook) => {
|
|
return {
|
|
...acc,
|
|
[webhook.type]: {
|
|
post: {
|
|
...parseOverload({
|
|
metadata: webhook.metadata,
|
|
method: 'POST',
|
|
path: `/webhooks/${webhook.type}`,
|
|
requestBodyDesc: undefinedIfMixed(yupObject({
|
|
type: yupString().defined().meta({ openapiField: { description: webhook.type, exampleValue: webhook.type } }),
|
|
data: webhook.schema.defined(),
|
|
}).describe()) || yupObject().describe(),
|
|
responseVariants: [{
|
|
responseTypeDesc: yupString().oneOf(['json']).describe(),
|
|
statusCodeDesc: yupNumber().oneOf([200]).describe(),
|
|
}],
|
|
}),
|
|
operationId: webhook.type,
|
|
summary: webhook.type,
|
|
}
|
|
},
|
|
};
|
|
}, {}),
|
|
};
|
|
}
|
|
|
|
function undefinedIfMixed(value: yup.SchemaFieldDescription | undefined): yup.SchemaFieldDescription | undefined {
|
|
if (!value) return undefined;
|
|
return value.type === 'mixed' ? undefined : value;
|
|
}
|
|
|
|
function isSchemaObjectDescription(value: yup.SchemaFieldDescription): value is yup.SchemaObjectDescription & { type: 'object' } {
|
|
return value.type === 'object';
|
|
}
|
|
|
|
function isSchemaMixedDescription(value: yup.SchemaFieldDescription): value is yup.SchemaDescription & { type: 'mixed' } {
|
|
return value.type === 'mixed';
|
|
}
|
|
|
|
function isSchemaArrayDescription(value: yup.SchemaFieldDescription): value is yup.SchemaInnerTypeDescription & { type: 'array', innerType: yup.SchemaInnerTypeDescription } {
|
|
return value.type === 'array';
|
|
}
|
|
|
|
function isSchemaTupleDescription(value: yup.SchemaFieldDescription): value is yup.SchemaInnerTypeDescription & { type: 'tuple', innerType: yup.SchemaInnerTypeDescription[] } {
|
|
return value.type === 'tuple';
|
|
}
|
|
|
|
function isSchemaStringDescription(value: yup.SchemaFieldDescription): value is yup.SchemaDescription & { type: 'string' } {
|
|
return value.type === 'string';
|
|
}
|
|
|
|
function isSchemaNumberDescription(value: yup.SchemaFieldDescription): value is yup.SchemaDescription & { type: 'number' } {
|
|
return value.type === 'number';
|
|
}
|
|
|
|
function isMaybeRequestSchemaForAudience(requestDescribe: yup.SchemaObjectDescription, audience: 'client' | 'server' | 'admin') {
|
|
const schemaAuth = requestDescribe.fields.auth;
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- yup types are wrong and claim that fields always exist
|
|
if (!schemaAuth) return true;
|
|
if (isSchemaMixedDescription(schemaAuth)) return true;
|
|
if (!isSchemaObjectDescription(schemaAuth)) return true;
|
|
const schemaAudience = schemaAuth.fields.type;
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- same as above
|
|
if (!schemaAudience) return true;
|
|
if ("oneOf" in schemaAudience && schemaAudience.oneOf.length > 0) {
|
|
return schemaAudience.oneOf.includes(audience);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function parseRouteHandler(options: {
|
|
handler: SmartRouteHandler,
|
|
path: string,
|
|
method: HttpMethod,
|
|
audience: 'client' | 'server' | 'admin',
|
|
}) {
|
|
let result: ReturnType<typeof parseOverload> | undefined;
|
|
|
|
for (const overload of options.handler.overloads.values()) {
|
|
if (overload.metadata?.hidden) continue;
|
|
|
|
const requestDescribe = overload.request.describe();
|
|
if (!isSchemaObjectDescription(requestDescribe)) throw new Error('Request schema must be a yup.ObjectSchema');
|
|
|
|
// estimate whether this overload is the right one based on a heuristic
|
|
if (!isMaybeRequestSchemaForAudience(requestDescribe, options.audience)) {
|
|
// This overload is definitely not for the audience
|
|
continue;
|
|
}
|
|
|
|
if (result) {
|
|
throw new HexclaveAssertionError(deindent`
|
|
OpenAPI generator matched multiple overloads for audience ${options.audience} on endpoint ${options.method} ${options.path}.
|
|
|
|
This does not necessarily mean there is a bug in the endpoint; the OpenAPI generator uses a heuristic to pick the allowed overloads, and may pick too many. Currently, this heuristic checks whether the request.auth.type property in the schema is a yup.string.oneOf(...) and matches it to the expected audience of the schema. If there are multiple overloads matching a single audience, for example because none of the overloads specify request.auth.type, the OpenAPI generator will not know which overload to generate specs for, and hence fails.
|
|
|
|
Either specify request.auth.type on the schema of the specified endpoint or update the OpenAPI generator to support your use case.
|
|
`);
|
|
}
|
|
|
|
const responseSchemaInfo = overload.response.meta()?.hexclaveSchemaInfo;
|
|
const responseSchemas: yup.AnySchema[] = responseSchemaInfo?.type === "union" ? responseSchemaInfo.items : [overload.response];
|
|
|
|
result = parseOverload({
|
|
metadata: overload.metadata,
|
|
method: options.method,
|
|
path: options.path,
|
|
pathDesc: undefinedIfMixed(requestDescribe.fields.params),
|
|
parameterDesc: undefinedIfMixed(requestDescribe.fields.query),
|
|
headerDesc: undefinedIfMixed(requestDescribe.fields.headers),
|
|
requestBodyDesc: undefinedIfMixed(requestDescribe.fields.body),
|
|
responseVariants: responseSchemas.map((schema) => {
|
|
const responseDescribe = schema.describe();
|
|
if (!isSchemaObjectDescription(responseDescribe)) {
|
|
throw new Error('Response schema must be a yup.ObjectSchema');
|
|
}
|
|
return {
|
|
responseDesc: undefinedIfMixed(responseDescribe.fields.body),
|
|
responseTypeDesc: undefinedIfMixed(responseDescribe.fields.bodyType) ?? throwErr('Response type must be defined and not mixed', { options, bodyTypeField: responseDescribe.fields.bodyType }),
|
|
statusCodeDesc: undefinedIfMixed(responseDescribe.fields.statusCode) ?? throwErr('Status code must be defined and not mixed', { options, statusCodeField: responseDescribe.fields.statusCode }),
|
|
};
|
|
}),
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function getFieldSchema(field: yup.SchemaFieldDescription, crudOperation?: Capitalize<CrudlOperation>): { type: string, items?: any, properties?: any, required?: any, default?: any } | undefined {
|
|
const meta = "meta" in field ? field.meta : {};
|
|
if (meta?.openapiField?.hidden) {
|
|
return undefined;
|
|
}
|
|
|
|
if (meta?.openapiField?.onlyShowInOperations && !meta.openapiField.onlyShowInOperations.includes(crudOperation as any)) {
|
|
return undefined;
|
|
}
|
|
|
|
const openapiFieldExtra = {
|
|
example: meta?.openapiField?.exampleValue,
|
|
description: meta?.openapiField?.description,
|
|
default: (field as any).default,
|
|
};
|
|
|
|
switch (field.type) {
|
|
case 'string': {
|
|
const oneOf = (field as any).oneOf as unknown[] | undefined;
|
|
return {
|
|
type: 'string',
|
|
...oneOf && oneOf.length > 0 ? { enum: oneOf } : {},
|
|
...openapiFieldExtra,
|
|
};
|
|
}
|
|
case 'number': {
|
|
const tests = (field as any).tests as Array<{ name?: string }> | undefined;
|
|
const isInteger = tests?.some(t => t.name === 'integer') ?? false;
|
|
return { type: isInteger ? 'integer' : 'number', ...openapiFieldExtra };
|
|
}
|
|
case 'boolean': {
|
|
return { type: field.type, ...openapiFieldExtra };
|
|
}
|
|
case 'mixed': {
|
|
return { type: 'object', ...openapiFieldExtra };
|
|
}
|
|
case 'object': {
|
|
return {
|
|
type: 'object',
|
|
properties: typedFromEntries(typedEntries((field as any).fields)
|
|
.map(([key, field]) => [key, getFieldSchema(field, crudOperation)])),
|
|
required: typedEntries((field as any).fields)
|
|
.filter(([_, field]) => !(field as any).optional && !(field as any).nullable && getFieldSchema(field as any, crudOperation))
|
|
.map(([key]) => key),
|
|
...openapiFieldExtra
|
|
};
|
|
}
|
|
case 'array': {
|
|
return { type: 'array', items: getFieldSchema((field as any).innerType, crudOperation), ...openapiFieldExtra };
|
|
}
|
|
case 'tuple': {
|
|
// For OpenAPI, treat tuples as arrays since OpenAPI doesn't have native tuple support
|
|
// This is commonly used for headers which are arrays of strings
|
|
const tupleField = field as any;
|
|
if (tupleField.innerType && tupleField.innerType.length > 0) {
|
|
// Use the first element's schema as the array item type
|
|
return { type: 'array', items: getFieldSchema(tupleField.innerType[0], crudOperation), ...openapiFieldExtra };
|
|
}
|
|
// Fallback to string array if no inner type
|
|
return { type: 'array', items: { type: 'string' }, ...openapiFieldExtra };
|
|
}
|
|
default: {
|
|
throw new Error(`Unsupported field type: ${field.type}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function toParameters(description: yup.SchemaFieldDescription, crudOperation?: Capitalize<CrudlOperation>, path?: string) {
|
|
const pathParams: string[] = path ? path.match(/{[^}]+}/g) || [] : [];
|
|
if (!isSchemaObjectDescription(description)) {
|
|
throw new HexclaveAssertionError('Parameters field must be an object schema', { actual: description });
|
|
}
|
|
|
|
return Object.entries(description.fields).map(([key, field]) => {
|
|
if (path && !pathParams.includes(`{${key}}`)) {
|
|
return { schema: undefined };
|
|
}
|
|
|
|
const meta = "meta" in field ? field.meta : {};
|
|
const schema = getFieldSchema(field, crudOperation);
|
|
return {
|
|
name: key,
|
|
in: path ? 'path' : 'query',
|
|
schema,
|
|
description: meta?.openapiField?.description,
|
|
required: !(field as any).optional && !!schema,
|
|
};
|
|
}).filter((x) => x.schema !== undefined);
|
|
}
|
|
|
|
function toHeaderParameters(description: yup.SchemaFieldDescription, crudOperation?: Capitalize<CrudlOperation>) {
|
|
if (!isSchemaObjectDescription(description)) {
|
|
throw new HexclaveAssertionError('Parameters field must be an object schema', { actual: description });
|
|
}
|
|
|
|
return Object.entries(description.fields).map(([key, tupleField]) => {
|
|
if (!isSchemaTupleDescription(tupleField)) {
|
|
throw new HexclaveAssertionError('Header field must be a tuple schema', { actual: tupleField, key });
|
|
}
|
|
if (tupleField.innerType.length !== 1) {
|
|
throw new HexclaveAssertionError('Header fields of length !== 1 not currently supported', { actual: tupleField, key });
|
|
}
|
|
const field = tupleField.innerType[0];
|
|
const meta = "meta" in field ? field.meta : {};
|
|
const schema = getFieldSchema(field, crudOperation);
|
|
return {
|
|
name: key,
|
|
in: 'header',
|
|
schema,
|
|
description: meta?.openapiField?.description,
|
|
example: meta?.openapiField?.exampleValue,
|
|
required: !(field as any).optional && !(field as any).nullable && !!schema,
|
|
};
|
|
}).filter((x) => x.schema !== undefined);
|
|
}
|
|
|
|
function toSchema(description: yup.SchemaFieldDescription, crudOperation?: Capitalize<CrudlOperation>): any {
|
|
if (isSchemaObjectDescription(description)) {
|
|
return {
|
|
type: 'object',
|
|
properties: Object.fromEntries(Object.entries(description.fields).map(([key, field]) => {
|
|
return [key, getFieldSchema(field, crudOperation)];
|
|
}, {}))
|
|
};
|
|
} else if (isSchemaArrayDescription(description)) {
|
|
return {
|
|
type: 'array',
|
|
items: toSchema(description.innerType, crudOperation),
|
|
};
|
|
} else {
|
|
throw new HexclaveAssertionError(`Unsupported schema type in toSchema: ${description.type}`, { actual: description });
|
|
}
|
|
}
|
|
|
|
function toRequired(description: yup.SchemaFieldDescription, crudOperation?: Capitalize<CrudlOperation>) {
|
|
let res: string[] = [];
|
|
if (isSchemaObjectDescription(description)) {
|
|
res = Object.entries(description.fields)
|
|
.filter(([_, field]) => !(field as any).optional && !(field as any).nullable && getFieldSchema(field, crudOperation))
|
|
.map(([key]) => key);
|
|
} else if (isSchemaArrayDescription(description)) {
|
|
res = [];
|
|
} else {
|
|
throw new HexclaveAssertionError(`Unsupported schema type in toRequired: ${description.type}`, { actual: description });
|
|
}
|
|
if (res.length === 0) return undefined;
|
|
return res;
|
|
}
|
|
|
|
function toExamples(description: yup.SchemaFieldDescription, crudOperation?: Capitalize<CrudlOperation>) {
|
|
if (!isSchemaObjectDescription(description)) {
|
|
throw new HexclaveAssertionError('Examples field must be an object schema', { actual: description });
|
|
}
|
|
|
|
return Object.entries(description.fields).reduce((acc, [key, field]) => {
|
|
const schema = getFieldSchema(field, crudOperation);
|
|
if (!schema) return acc;
|
|
const example = "meta" in field ? field.meta?.openapiField?.exampleValue : undefined;
|
|
return { ...acc, [key]: example };
|
|
}, {});
|
|
}
|
|
|
|
export function parseOverload(options: {
|
|
metadata: EndpointDocumentation | undefined,
|
|
method: string,
|
|
path: string,
|
|
pathDesc?: yup.SchemaFieldDescription,
|
|
parameterDesc?: yup.SchemaFieldDescription,
|
|
headerDesc?: yup.SchemaFieldDescription,
|
|
requestBodyDesc?: yup.SchemaFieldDescription,
|
|
responseVariants: Array<{
|
|
responseDesc?: yup.SchemaFieldDescription,
|
|
responseTypeDesc: yup.SchemaFieldDescription,
|
|
statusCodeDesc: yup.SchemaFieldDescription,
|
|
}>,
|
|
}) {
|
|
const endpointDocumentation = options.metadata ?? {
|
|
summary: `${options.method} ${options.path}`,
|
|
description: `No documentation available for this endpoint.`,
|
|
};
|
|
if (endpointDocumentation.hidden) {
|
|
return undefined;
|
|
}
|
|
|
|
const pathParameters = options.pathDesc ? toParameters(options.pathDesc, endpointDocumentation.crudOperation, options.path) : [];
|
|
const queryParameters = options.parameterDesc ? toParameters(options.parameterDesc, endpointDocumentation.crudOperation) : [];
|
|
const headerParameters = options.headerDesc ? toHeaderParameters(options.headerDesc, endpointDocumentation.crudOperation) : [];
|
|
|
|
let requestBody;
|
|
if (options.requestBodyDesc) {
|
|
requestBody = {
|
|
required: true,
|
|
content: {
|
|
'application/json': {
|
|
schema: {
|
|
...toSchema(options.requestBodyDesc, endpointDocumentation.crudOperation),
|
|
required: toRequired(options.requestBodyDesc, endpointDocumentation.crudOperation),
|
|
example: toExamples(options.requestBodyDesc, endpointDocumentation.crudOperation),
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
const allResponses: Record<number, unknown> = {};
|
|
|
|
for (const { responseDesc, responseTypeDesc, statusCodeDesc } of options.responseVariants) {
|
|
if (!isSchemaStringDescription(responseTypeDesc)) {
|
|
throw new HexclaveAssertionError(`Expected response type to be a string`, { actual: responseTypeDesc, options });
|
|
}
|
|
if (responseTypeDesc.oneOf.length !== 1) {
|
|
throw new HexclaveAssertionError(`Expected response type to have exactly one value`, { actual: responseTypeDesc, options });
|
|
}
|
|
const bodyType = responseTypeDesc.oneOf[0];
|
|
|
|
if (!isSchemaNumberDescription(statusCodeDesc)) {
|
|
throw new HexclaveAssertionError('Expected status code to be a number', { actual: statusCodeDesc, options });
|
|
}
|
|
|
|
// Get all status codes or use 200 as default if none specified
|
|
const statusCodes: number[] = statusCodeDesc.oneOf.length > 0
|
|
? statusCodeDesc.oneOf as number[]
|
|
: [200]; // TODO HACK hardcoded, used in case all status codes may be returned, should be configurable per endpoint
|
|
|
|
for (const status of statusCodes) {
|
|
switch (bodyType) {
|
|
case 'json': {
|
|
allResponses[status] = {
|
|
description: 'Successful response',
|
|
content: {
|
|
'application/json': {
|
|
schema: {
|
|
...responseDesc ? toSchema(responseDesc, endpointDocumentation.crudOperation) : {},
|
|
required: responseDesc ? toRequired(responseDesc, endpointDocumentation.crudOperation) : undefined,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
break;
|
|
}
|
|
case 'text': {
|
|
if (!responseDesc || !isSchemaStringDescription(responseDesc)) {
|
|
throw new HexclaveAssertionError('Expected response body of bodyType=="text" to be a string schema', { actual: responseDesc });
|
|
}
|
|
allResponses[status] = {
|
|
description: 'Successful response',
|
|
content: {
|
|
'text/plain': {
|
|
schema: {
|
|
type: 'string',
|
|
example: isSchemaStringDescription(responseDesc) ? responseDesc.meta?.openapiField?.exampleValue : undefined,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
break;
|
|
}
|
|
case 'success': {
|
|
allResponses[status] = {
|
|
description: 'Successful response',
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
success: {
|
|
type: "boolean",
|
|
description: "Always equal to true.",
|
|
example: true,
|
|
},
|
|
},
|
|
required: ["success"],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
break;
|
|
}
|
|
case 'empty': {
|
|
allResponses[status] = {
|
|
description: 'No content',
|
|
};
|
|
break;
|
|
}
|
|
default: {
|
|
throw new HexclaveAssertionError(`Unsupported body type: ${bodyType}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
summary: endpointDocumentation.summary,
|
|
description: endpointDocumentation.description,
|
|
parameters: [...queryParameters, ...pathParameters, ...headerParameters],
|
|
requestBody,
|
|
tags: endpointDocumentation.tags ?? ["Others"],
|
|
'x-full-url': `https://api.hexclave.com/api/v1${options.path}`,
|
|
responses: allResponses,
|
|
};
|
|
}
|