UNPKG

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
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 ? `![Unleash Enterprise](${openapiStaticAssets}/Enterprise.svg) **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