mirror of
https://github.com/Unleash/unleash.git
synced 2026-02-04 20:10:52 +01:00
Merge 305708013e into 01912ba6a3
This commit is contained in:
commit
3da516a7b8
@ -30,6 +30,7 @@ We are in the process of defining ADRs for the back end. At the time of writing
|
||||
* [Write model vs Read models](/contributing/ADRs/back-end/write-model-vs-read-models)
|
||||
* [Frontend API Design](/contributing/ADRs/back-end/frontend-api-design)
|
||||
* [Correct type dependencies](/contributing/ADRs/back-end/correct-type-dependencies)
|
||||
* [API Version Tracking and Stability Lifecycle](/contributing/ADRs/back-end/api-version-tracking)
|
||||
|
||||
## Front-end ADRs
|
||||
|
||||
|
||||
102
contributing/ADRs/back-end/api-version-tracking.md
Normal file
102
contributing/ADRs/back-end/api-version-tracking.md
Normal file
@ -0,0 +1,102 @@
|
||||
---
|
||||
title: "ADR: API Version Tracking and Stability Lifecycle"
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Our OpenAPI documentation lacked a systematic way to communicate API maturity and stability to users. We had no automated mechanism to:
|
||||
- Track when APIs were introduced
|
||||
- Communicate stability levels (alpha, beta, stable) to API consumers
|
||||
- Filter unreleased APIs from public documentation
|
||||
- Provide historical context about API evolution
|
||||
|
||||
Manually maintaining stability levels was error-prone and often forgotten. APIs would remain marked as "beta" indefinitely, or lack any stability indicator altogether. This created confusion for both internal developers and external API consumers about which endpoints were production-ready.
|
||||
|
||||
Additionally, we wanted to ship and test new APIs with select customers before formally documenting them, but had no way to hide alpha/unreleased features from public docs while keeping them functionally available.
|
||||
|
||||
## Decision
|
||||
|
||||
We've implemented an automated API stability tracking system based on semantic versioning. Each endpoint can declare an `alphaUntilVersion` and/or a `betaUntilVersion` to define cutoffs. The system calculates stability levels by comparing those cutoffs against the current version:
|
||||
|
||||
### Stability Calculation Heuristic
|
||||
|
||||
- **Alpha** 🔴: Current version is before `alphaUntilVersion` (when defined)
|
||||
- **Beta** 🟡: Current version is at or after `alphaUntilVersion` but before `betaUntilVersion` (when defined)
|
||||
- **Stable** 🟢: Current version is at or after `betaUntilVersion` (when defined); if `betaUntilVersion` is omitted, stability is reached once alpha is no longer in effect (or immediately if `alphaUntilVersion` is also omitted)
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// Current Unleash version: 7.4.0
|
||||
|
||||
openApiService.validPath({
|
||||
tags: ['Features'],
|
||||
summary: 'Create feature flag',
|
||||
alphaUntilVersion: '7.5.0',
|
||||
betaUntilVersion: '7.7.0', // → Alpha (not yet released)
|
||||
operationId: 'createFeature',
|
||||
// ...
|
||||
})
|
||||
|
||||
openApiService.validPath({
|
||||
tags: ['Projects'],
|
||||
summary: 'List projects',
|
||||
alphaUntilVersion: '7.3.0',
|
||||
betaUntilVersion: '7.5.0', // → Beta (between alpha and beta cutoffs)
|
||||
operationId: 'getProjects',
|
||||
// ...
|
||||
})
|
||||
|
||||
openApiService.validPath({
|
||||
tags: ['Users'],
|
||||
summary: 'Get user info',
|
||||
betaUntilVersion: '7.1.0', // → Stable (already at/after beta cutoff)
|
||||
operationId: 'getUserInfo',
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
### Implementation
|
||||
|
||||
1. **ApiOperation Interface**: Added `alphaUntilVersion?: string` and `betaUntilVersion?: string` (omit both to mark stable). `releaseVersion?: string` is available for documentation only.
|
||||
2. **Stability Calculation**: `calculateStability()` compares alpha/beta cutoffs with the current Unleash version
|
||||
3. **OpenAPI Extensions**: Adds only `x-stability-level` to the OpenAPI spec (cutoff versions stay in code as documentation)
|
||||
4. **Swagger UI Integration**:
|
||||
- Alpha endpoints are hidden from public docs in production
|
||||
- Visible in development mode (`NODE_ENV=development`)
|
||||
- Stability prefix added to endpoint summaries (e.g., `[BETA] List projects`)
|
||||
|
||||
### Alpha API Behavior
|
||||
|
||||
Alpha endpoints are **automatically hidden** from the public OpenAPI docs (`/docs/openapi`) in production. This means:
|
||||
|
||||
- ✅ **APIs are still fully functional** - clients can use them if they know the endpoint
|
||||
- 📖 **Not advertised publicly** - they don't appear in the documentation portal
|
||||
- 🔍 **Visible in development** - when `NODE_ENV=development`, all alpha endpoints show up for internal testing
|
||||
|
||||
This gives us the best of both worlds: we can ship and test alpha APIs internally without formally documenting them, reducing support burden and managing expectations for unstable features.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
**Zero maintenance**: As Unleash versions progress, APIs automatically transition from alpha → beta → stable based on the declared milestones.
|
||||
|
||||
**Built-in documentation**: The cutoff versions (and optional `releaseVersion`) serve as lifecycle documentation. Anyone can see the intended lifecycle and assess maturity.
|
||||
|
||||
**Flexible during development**: Developers estimate which versions a new API will reach beta and stable. If priorities change or development takes longer, they update the milestones.
|
||||
|
||||
**Selective disclosure**: Ship alpha features to production for testing without exposing them to customers, until the API is ready to be moved to beta.
|
||||
|
||||
**Consistency**: Every API follows the same maturity progression, eliminating confusion about stability levels.
|
||||
|
||||
### Migration Path
|
||||
|
||||
1. **Immediate**: New endpoints can include `alphaUntilVersion` and/or `betaUntilVersion` as needed
|
||||
2. **Gradual**: Add cutoffs to existing endpoints as they're modified
|
||||
3. **Future**: AI-assisted bulk backfill from git history to document all existing APIs
|
||||
|
||||
### Trade-offs
|
||||
|
||||
**Milestone guessing required**: Developers must estimate beta/stable milestones during development. This is acceptable given the milestones can be updated.
|
||||
|
||||
**Explicit lifecycle**: Cutoffs are explicit, but they require setting (and occasionally updating) the versions.
|
||||
@ -91,7 +91,7 @@ export class ContextController extends Controller {
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Context'],
|
||||
beta,
|
||||
betaUntilVersion: '7.6.0', // two versions ahead of current
|
||||
summary: 'Gets configured context fields',
|
||||
description:
|
||||
'Returns all configured [Context fields](https://docs.getunleash.io/concepts/unleash-context) that have been created.',
|
||||
@ -112,7 +112,7 @@ export class ContextController extends Controller {
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Context'],
|
||||
beta,
|
||||
betaUntilVersion: '7.6.0', // two versions ahead of current
|
||||
summary: 'Gets context field',
|
||||
description:
|
||||
'Returns specific [context field](https://docs.getunleash.io/concepts/unleash-context) identified by the name in the path',
|
||||
@ -132,7 +132,7 @@ export class ContextController extends Controller {
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Strategies'],
|
||||
beta,
|
||||
betaUntilVersion: '7.6.0', // two versions ahead of current
|
||||
operationId: resolveOperationId(
|
||||
'getStrategiesByContextField',
|
||||
mode,
|
||||
@ -158,7 +158,7 @@ export class ContextController extends Controller {
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Context'],
|
||||
beta,
|
||||
betaUntilVersion: '7.6.0', // two versions ahead of current
|
||||
operationId: resolveOperationId('createContextField', mode),
|
||||
summary: 'Create a context field',
|
||||
description:
|
||||
@ -183,7 +183,7 @@ export class ContextController extends Controller {
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Context'],
|
||||
beta,
|
||||
betaUntilVersion: '7.6.0', // two versions ahead of current
|
||||
summary: 'Update an existing context field',
|
||||
description: `Endpoint that allows updating a custom context field. Used to toggle stickiness and add/remove legal values for this context field`,
|
||||
operationId: resolveOperationId('updateContextField', mode),
|
||||
@ -205,7 +205,7 @@ export class ContextController extends Controller {
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Context'],
|
||||
beta,
|
||||
betaUntilVersion: '7.6.0', // two versions ahead of current
|
||||
summary: 'Add or update legal value for the context field',
|
||||
description: `Endpoint that allows adding or updating a single custom context field legal value. If the legal value already exists, it will be updated with the new description`,
|
||||
operationId: resolveOperationId(
|
||||
@ -229,7 +229,7 @@ export class ContextController extends Controller {
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Context'],
|
||||
beta,
|
||||
betaUntilVersion: '7.6.0', // two versions ahead of current
|
||||
summary: 'Delete legal value for the context field',
|
||||
description: `Removes the specified custom context field legal value. Does not validate that the legal value is not in use and does not remove usage from constraints that use it.`,
|
||||
operationId: resolveOperationId(
|
||||
@ -252,7 +252,7 @@ export class ContextController extends Controller {
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Context'],
|
||||
beta,
|
||||
betaUntilVersion: '7.6.0', // two versions ahead of current
|
||||
summary: 'Delete an existing context field',
|
||||
description:
|
||||
'Endpoint that allows deletion of a custom context field. Does not validate that context field is not in use, but since context field configuration is stored in a json blob for the strategy, existing strategies are safe.',
|
||||
@ -272,7 +272,7 @@ export class ContextController extends Controller {
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Context'],
|
||||
beta,
|
||||
betaUntilVersion: '7.6.0', // two versions ahead of current
|
||||
summary: 'Validate a context field',
|
||||
description:
|
||||
'Check whether the provided data can be used to create a context field. If the data is not valid, returns a 400 status code with the reason why it is not valid.',
|
||||
|
||||
81
src/lib/openapi/util/api-operation.test.ts
Normal file
81
src/lib/openapi/util/api-operation.test.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { calculateStability } from './api-operation.js';
|
||||
|
||||
test('calculateStability returns alpha when current is before alpha cutoff', () => {
|
||||
expect(
|
||||
calculateStability({
|
||||
alphaUntilVersion: '7.5.0',
|
||||
betaUntilVersion: '7.7.0',
|
||||
currentVersion: '7.4.0',
|
||||
}),
|
||||
).toBe('alpha');
|
||||
});
|
||||
|
||||
test('calculateStability returns beta when current is between alpha and beta cutoffs', () => {
|
||||
expect(
|
||||
calculateStability({
|
||||
alphaUntilVersion: '7.5.0',
|
||||
betaUntilVersion: '7.7.0',
|
||||
currentVersion: '7.6.0',
|
||||
}),
|
||||
).toBe('beta');
|
||||
});
|
||||
|
||||
test('calculateStability returns stable when current is at or after beta cutoff', () => {
|
||||
expect(
|
||||
calculateStability({
|
||||
alphaUntilVersion: '7.5.0',
|
||||
betaUntilVersion: '7.7.0',
|
||||
currentVersion: '7.7.0',
|
||||
}),
|
||||
).toBe('stable');
|
||||
expect(
|
||||
calculateStability({
|
||||
alphaUntilVersion: '7.5.0',
|
||||
betaUntilVersion: '7.7.0',
|
||||
currentVersion: '7.8.0',
|
||||
}),
|
||||
).toBe('stable');
|
||||
});
|
||||
|
||||
test('calculateStability returns beta when beta cutoff is set without alpha', () => {
|
||||
expect(
|
||||
calculateStability({
|
||||
betaUntilVersion: '7.6.0',
|
||||
currentVersion: '7.4.0',
|
||||
}),
|
||||
).toBe('beta');
|
||||
});
|
||||
|
||||
test('calculateStability returns stable when beta cutoff is omitted and current is after alpha', () => {
|
||||
expect(
|
||||
calculateStability({
|
||||
alphaUntilVersion: '7.5.0',
|
||||
currentVersion: '7.6.0',
|
||||
}),
|
||||
).toBe('stable');
|
||||
});
|
||||
|
||||
test('calculateStability returns stable when alpha and beta cutoffs are omitted', () => {
|
||||
expect(
|
||||
calculateStability({
|
||||
currentVersion: '7.8.0',
|
||||
}),
|
||||
).toBe('stable');
|
||||
});
|
||||
|
||||
test('calculateStability defaults to stable when versions are invalid', () => {
|
||||
expect(
|
||||
calculateStability({
|
||||
alphaUntilVersion: 'not-a-version',
|
||||
betaUntilVersion: 'x.y.z',
|
||||
currentVersion: '7.4.0',
|
||||
}),
|
||||
).toBe('stable');
|
||||
expect(
|
||||
calculateStability({
|
||||
alphaUntilVersion: '7.5.0',
|
||||
betaUntilVersion: '7.7.0',
|
||||
currentVersion: 'nope',
|
||||
}),
|
||||
).toBe('stable');
|
||||
});
|
||||
@ -1,5 +1,48 @@
|
||||
import type { OpenAPIV3 } from 'openapi-types';
|
||||
import type { OpenApiTag } from './openapi-tags.js';
|
||||
import semver from 'semver';
|
||||
|
||||
/**
|
||||
* Calculate stability level based on comparing alpha/beta cutoffs
|
||||
* against the current version.
|
||||
* - Alpha: current version is before alpha cutoff (when defined)
|
||||
* - Beta: current version is at/after alpha cutoff and before beta cutoff (when defined)
|
||||
* - Stable: current version is at/after beta cutoff (when defined), or when no cutoffs apply
|
||||
*/
|
||||
type StabilityVersions = {
|
||||
alphaUntilVersion?: string;
|
||||
betaUntilVersion?: string;
|
||||
currentVersion: string;
|
||||
};
|
||||
|
||||
export function calculateStability({
|
||||
alphaUntilVersion,
|
||||
betaUntilVersion,
|
||||
currentVersion,
|
||||
}: StabilityVersions): 'alpha' | 'beta' | 'stable' {
|
||||
if (!alphaUntilVersion && !betaUntilVersion) {
|
||||
return 'stable';
|
||||
}
|
||||
const current = semver.coerce(currentVersion);
|
||||
if (!current) {
|
||||
return 'stable'; // Default to stable if current can't be parsed
|
||||
}
|
||||
|
||||
const alphaUntil = alphaUntilVersion
|
||||
? semver.coerce(alphaUntilVersion)
|
||||
: null;
|
||||
const betaUntil = betaUntilVersion ? semver.coerce(betaUntilVersion) : null;
|
||||
|
||||
if (alphaUntil && semver.lt(current, alphaUntil)) {
|
||||
return 'alpha';
|
||||
}
|
||||
|
||||
if (betaUntil && semver.lt(current, betaUntil)) {
|
||||
return 'beta';
|
||||
}
|
||||
|
||||
return 'stable';
|
||||
}
|
||||
|
||||
type DeprecatedOpenAPITag =
|
||||
// Deprecated tag names. Please use a tag from the OpenAPITag type instead.
|
||||
@ -14,6 +57,20 @@ export interface ApiOperation<Tag = OpenApiTag | DeprecatedOpenAPITag>
|
||||
extends Omit<OpenAPIV3.OperationObject, 'tags'> {
|
||||
operationId: string;
|
||||
tags: [Tag];
|
||||
beta?: boolean;
|
||||
/**
|
||||
* The version up to (but not including) which this API is alpha.
|
||||
* If omitted, the API is never alpha.
|
||||
*/
|
||||
alphaUntilVersion?: string;
|
||||
/**
|
||||
* The version up to (but not including) which this API is beta.
|
||||
* If omitted, the API is stable once it is no longer alpha.
|
||||
*/
|
||||
betaUntilVersion?: string;
|
||||
/**
|
||||
* The version when this API was introduced or last significantly changed.
|
||||
* Documentation only; does not affect stability calculation.
|
||||
*/
|
||||
releaseVersion?: string;
|
||||
enterpriseOnly?: boolean;
|
||||
}
|
||||
|
||||
55
src/lib/services/openapi-service.test.ts
Normal file
55
src/lib/services/openapi-service.test.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import type { OpenAPIV3 } from 'openapi-types';
|
||||
import { OpenApiService } from './openapi-service.js';
|
||||
import { createTestConfig } from '../../test/config/test-config.js';
|
||||
|
||||
const okResponse = { '200': { description: 'ok' } };
|
||||
type OperationWithStability = OpenAPIV3.OperationObject & {
|
||||
'x-stability-level'?: string;
|
||||
};
|
||||
|
||||
const buildDocument = (): OpenAPIV3.Document => ({
|
||||
openapi: '3.0.0',
|
||||
info: { title: 'Test API', version: '7.4.0' },
|
||||
paths: {
|
||||
'/alpha-only': {
|
||||
get: {
|
||||
responses: okResponse,
|
||||
'x-stability-level': 'alpha',
|
||||
} as OperationWithStability,
|
||||
},
|
||||
'/mixed': {
|
||||
get: {
|
||||
responses: okResponse,
|
||||
'x-stability-level': 'alpha',
|
||||
} as OperationWithStability,
|
||||
post: {
|
||||
responses: okResponse,
|
||||
'x-stability-level': 'beta',
|
||||
} as OperationWithStability,
|
||||
},
|
||||
'/stable': {
|
||||
put: {
|
||||
responses: okResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
test('removeAlphaOperations removes alpha operations and empty paths', () => {
|
||||
const openApiService = new OpenApiService(createTestConfig());
|
||||
const removeAlphaOperations = (
|
||||
openApiService as unknown as {
|
||||
removeAlphaOperations: (
|
||||
doc: OpenAPIV3.Document,
|
||||
) => OpenAPIV3.Document;
|
||||
}
|
||||
).removeAlphaOperations.bind(openApiService);
|
||||
|
||||
const doc = buildDocument();
|
||||
const filtered = removeAlphaOperations(doc);
|
||||
|
||||
expect(filtered.paths?.['/alpha-only']).toBeUndefined();
|
||||
expect(filtered.paths?.['/mixed']?.get).toBeUndefined();
|
||||
expect(filtered.paths?.['/mixed']?.post).toBeDefined();
|
||||
expect(filtered.paths?.['/stable']).toBeDefined();
|
||||
});
|
||||
@ -1,5 +1,6 @@
|
||||
import openapi, { type IExpressOpenApi } from '@wesleytodd/openapi';
|
||||
import type { Express, RequestHandler, Response } from 'express';
|
||||
import type { OpenAPIV3 } from 'openapi-types';
|
||||
import type { IUnleashConfig } from '../types/option.js';
|
||||
import {
|
||||
createOpenApiSchema,
|
||||
@ -7,17 +8,43 @@ import {
|
||||
removeJsonSchemaProps,
|
||||
type SchemaId,
|
||||
} from '../openapi/index.js';
|
||||
import type { ApiOperation } from '../openapi/util/api-operation.js';
|
||||
import {
|
||||
type ApiOperation,
|
||||
calculateStability,
|
||||
} from '../openapi/util/api-operation.js';
|
||||
import type { Logger } from '../logger.js';
|
||||
import { validateSchema } from '../openapi/validate.js';
|
||||
import type { IFlagResolver } from '../types/index.js';
|
||||
import packageVersion from '../util/version.js';
|
||||
const getStabilityLevel = (operation: unknown): string | undefined => {
|
||||
if (!operation || typeof operation !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
operation as OpenAPIV3.OperationObject & {
|
||||
'x-stability-level'?: string;
|
||||
}
|
||||
)['x-stability-level'];
|
||||
};
|
||||
type OpenApiDocument = OpenAPIV3.Document;
|
||||
type OpenApiMiddleware = IExpressOpenApi & {
|
||||
document: OpenApiDocument;
|
||||
generateDocument: (
|
||||
baseDocument: OpenApiDocument,
|
||||
router?: unknown,
|
||||
basePath?: string,
|
||||
) => OpenApiDocument;
|
||||
};
|
||||
|
||||
export class OpenApiService {
|
||||
private readonly config: IUnleashConfig;
|
||||
|
||||
private readonly logger: Logger;
|
||||
|
||||
private readonly api: IExpressOpenApi;
|
||||
private readonly api: OpenApiMiddleware;
|
||||
|
||||
private readonly isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
private flagResolver: IFlagResolver;
|
||||
|
||||
@ -34,26 +61,38 @@ export class OpenApiService {
|
||||
extendRefs: true,
|
||||
basePath: config.server.baseUriPath,
|
||||
},
|
||||
);
|
||||
) as OpenApiMiddleware;
|
||||
}
|
||||
|
||||
validPath(op: ApiOperation): RequestHandler {
|
||||
const { beta, enterpriseOnly, ...rest } = op;
|
||||
const { alphaUntilVersion, betaUntilVersion, enterpriseOnly, ...rest } =
|
||||
op;
|
||||
const { baseUriPath = '' } = this.config.server ?? {};
|
||||
const openapiStaticAssets = `${baseUriPath}/openapi-static`;
|
||||
const betaBadge = beta
|
||||
? ` This is a beta endpoint and it may change or be removed in the future.
|
||||
|
||||
const currentVersion =
|
||||
this.api.document?.info?.version || packageVersion || '7.0.0';
|
||||
const stability = calculateStability({
|
||||
alphaUntilVersion,
|
||||
betaUntilVersion,
|
||||
currentVersion,
|
||||
});
|
||||
const summaryWithStability =
|
||||
stability !== 'stable' && rest.summary
|
||||
? `[${stability.toUpperCase()}] ${rest.summary}`
|
||||
: rest.summary;
|
||||
const stabilityBadge =
|
||||
stability !== 'stable'
|
||||
? `**[${stability.toUpperCase()}]** This is a ${stability} endpoint and it may change or be removed in the future.
|
||||
`
|
||||
: '';
|
||||
: '';
|
||||
const enterpriseBadge = enterpriseOnly
|
||||
? ` **Enterprise feature**
|
||||
|
||||
`
|
||||
: '';
|
||||
|
||||
const failDeprecated =
|
||||
(op.deprecated ?? false) && process.env.NODE_ENV === 'development';
|
||||
const failDeprecated = (op.deprecated ?? false) && this.isDevelopment;
|
||||
|
||||
if (failDeprecated) {
|
||||
return (req, res, _next) => {
|
||||
@ -67,8 +106,10 @@ export class OpenApiService {
|
||||
}
|
||||
return this.api.validPath({
|
||||
...rest,
|
||||
summary: summaryWithStability,
|
||||
'x-stability-level': stability,
|
||||
description:
|
||||
`${enterpriseBadge}${betaBadge}${op.description}`.replaceAll(
|
||||
`${enterpriseBadge}${stabilityBadge}${op.description}`.replaceAll(
|
||||
/\n\s*/g,
|
||||
'\n\n',
|
||||
),
|
||||
@ -76,10 +117,52 @@ export class OpenApiService {
|
||||
}
|
||||
|
||||
useDocs(app: Express): void {
|
||||
// Serve a filtered OpenAPI document that hides alpha endpoints from Swagger UI.
|
||||
app.get(`${this.docsPath()}.json`, (req, res, next) => {
|
||||
try {
|
||||
const doc = this.api.generateDocument(
|
||||
this.api.document,
|
||||
req.app._router || req.app.router,
|
||||
this.config.server.baseUriPath,
|
||||
);
|
||||
res.json(
|
||||
this.isDevelopment ? doc : this.removeAlphaOperations(doc),
|
||||
);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.use(this.api);
|
||||
app.use(this.docsPath(), this.api.swaggerui());
|
||||
}
|
||||
|
||||
// Remove operations explicitly marked as alpha to keep them out of the rendered docs.
|
||||
private removeAlphaOperations(doc: OpenApiDocument): OpenApiDocument {
|
||||
if (!doc?.paths) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
const filteredPaths: OpenAPIV3.PathsObject = {};
|
||||
for (const [path, methods] of Object.entries(doc.paths)) {
|
||||
if (!methods) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entries = Object.entries(methods).filter(
|
||||
([, operation]) => getStabilityLevel(operation) !== 'alpha',
|
||||
);
|
||||
|
||||
if (entries.length > 0) {
|
||||
filteredPaths[path] = Object.fromEntries(
|
||||
entries,
|
||||
) as OpenAPIV3.PathItemObject;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...doc, paths: filteredPaths };
|
||||
}
|
||||
|
||||
docsPath(): string {
|
||||
const { baseUriPath = '' } = this.config.server ?? {};
|
||||
return `${baseUriPath}/docs/openapi`;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user