unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
112 lines (111 loc) • 4.84 kB
JavaScript
import openapi from '@wesleytodd/openapi';
import { createOpenApiSchema, removeJsonSchemaProps, } from '../openapi/index.js';
import { validateSchema } from '../openapi/validate.js';
import { calculateStability } from '../openapi/util/api-stability.js';
const getStabilityLevel = (operation) => {
if (!operation || typeof operation !== 'object') {
return undefined;
}
return operation['x-stability-level'];
};
export class OpenApiService {
constructor(config) {
this.isDevelopment = process.env.NODE_ENV === 'development';
this.config = config;
this.flagResolver = config.flagResolver;
this.logger = config.getLogger('openapi-service.ts');
this.api = openapi(this.docsPath(), createOpenApiSchema(config.server), {
coerce: true,
extendRefs: true,
basePath: config.server.baseUriPath,
});
}
validPath(op) {
// extract enterpriseOnly and release to avoid leaking into the OpenAPI spec
const { enterpriseOnly, release, ...openapiSpec } = op;
const { baseUriPath = '' } = this.config.server ?? {};
const openapiStaticAssets = `${baseUriPath}/openapi-static`;
const currentVersion = this.api.document.info.version;
const stability = calculateStability(release, currentVersion);
const summaryWithStability = stability !== 'stable' && openapiSpec.summary
? `[${stability.toUpperCase()}] ${openapiSpec.summary}`
: openapiSpec.summary;
const stabilityBadge = stability !== 'stable'
? `**[${stability.toUpperCase()}]** This API is in ${stability} state, which means it may change or be removed in the future.
`
: '';
const enterpriseBadge = enterpriseOnly
? ` **Enterprise feature**
`
: '';
const failDeprecated = (op.deprecated ?? false) && this.isDevelopment;
if (failDeprecated) {
return (req, res, _next) => {
this.logger.warn(`Deprecated endpoint: ${op.operationId} at ${req.path}`);
return res.status(410).json({
message: `The endpoint ${op.operationId} at ${req.path} is deprecated and should not be used.`,
});
};
}
return this.api.validPath({
...openapiSpec,
summary: summaryWithStability,
'x-stability-level': stability,
description: `${enterpriseBadge}${stabilityBadge}${op.description}`.replaceAll(/\n\s*/g, '\n\n'),
});
}
useDocs(app) {
// 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.
removeAlphaOperations(doc) {
if (!doc?.paths) {
return doc;
}
const filteredPaths = {};
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);
}
}
return { ...doc, paths: filteredPaths };
}
docsPath() {
const { baseUriPath = '' } = this.config.server ?? {};
return `${baseUriPath}/docs/openapi`;
}
registerCustomSchemas(schemas) {
Object.entries(schemas).forEach(([name, schema]) => {
this.api.schema(name, removeJsonSchemaProps(schema));
});
}
respondWithValidation(status, res, schema, data, headers = {}) {
const errors = validateSchema(schema, data);
if (errors) {
this.logger.debug(`Invalid response for ${res.req?.originalUrl || ''}:`, errors);
if (this.flagResolver.isEnabled('strictSchemaValidation')) {
throw new Error(JSON.stringify(errors, null, 4));
}
}
Object.entries(headers).forEach(([header, value]) => {
res.header(header, value);
});
res.status(status).json(data);
}
}
//# sourceMappingURL=openapi-service.js.map