UNPKG

saasify-to-openapi

Version:

Converts Saasify deployments to OpenAPI specs.

274 lines (229 loc) 6.66 kB
'use strict' // TODO: switch to using https://github.com/openapi-contrib/json-schema-to-openapi-schema const jsonSchemaToOpenAPISchema = require('json-schema-to-openapi-schema') const pReduce = require('p-reduce') const { prepareJsonSchema } = require('./lib/prepare-json-schema') /** * Converts a Saasify Deployment into an OpenAPI spec. * * Note that this is only used for deployments powered by FTS, as all other * deployments are derived from an OpenAPI spec. * * @return {Promise} */ module.exports = async function saasifyToOpenAPI(deployment) { if (deployment.openapi) { return deployment.openapi } const components = { schemas: {} } const paths = await pReduce( deployment.services, async (paths, service) => { const newPaths = await module.exports.serviceToPaths(service, components) for (const path of Object.keys(newPaths)) { const newPath = newPaths[path] if (paths[path]) { paths[path] = { ...paths[path], ...newPath } } else { paths[path] = newPath } } return paths }, {} ) const version = deployment.version ? `v${deployment.version}` : undefined // all other saasify-specific metadata gets added to the this base openapi // spec later via annotate-openapi return { openapi: '3.0.2', info: { title: deployment.project.name, version }, paths, components } } module.exports.serviceToPaths = async function serviceToPaths( service, components ) { const { route, definition } = service const result = {} // --------------------------------------------------------------------------- // Parameters // --------------------------------------------------------------------------- const { http: isRawHttpRequest = false, schema: paramsSchema } = definition.params let requestBody let requestSchema if (isRawHttpRequest) { requestSchema = { type: 'string', format: 'binary', description: 'Raw HTTP request body which can be interpreted using the standard `Content-Type` header.' } requestBody = { required: true, content: { '*/*': { schema: requestSchema } } } } else { const params = await prepareJsonSchema(paramsSchema, components) requestSchema = jsonSchemaToOpenAPISchema(params) requestBody = { required: true, content: { 'application/json': { schema: requestSchema } } } } // --------------------------------------------------------------------------- // Example Usage // --------------------------------------------------------------------------- if (service.examples) { const examplesOrdered = service.examples.filter( (example) => !example.inputContentType || example.inputContentType === 'application/json' ) if (examplesOrdered.length) { const examples = examplesOrdered.reduce( (acc, example) => ({ ...acc, [example.name]: example.input }), {} ) // TODO: we'd really like to move this into annotate-openapi so all schemas // can benefit from it. // infer example values for all parameters from the list of provided example inputs for (const [name, schema] of Object.entries(requestSchema.properties)) { let found = false for (const example of Object.values(examples)) { for (const [key, value] of Object.entries(example)) { if (key === name) { schema.example = value found = true break } } if (found) { break } } } } } // --------------------------------------------------------------------------- // Responses // --------------------------------------------------------------------------- const { returns = {} } = definition const { http: isRawHttpResponse = false, schema: responseSchema } = returns let responses = { 200: { description: 'Success' } } if (responseSchema) { const { type, additionalProperties, properties, ...rest } = responseSchema if (isRawHttpResponse) { const responseSchema = { type: 'string', format: 'binary', description: 'Raw HTTP response body which can be interpreted using the standard `Content-Type` header.' } responses = { 200: { description: 'Success', content: { // TODO: support restricted response content-types via OpenAPI `produces` prop '*/*': { schema: responseSchema } } } } } else { const returnsJsonSchema = { ...rest, ...properties.result } const returns = await prepareJsonSchema(returnsJsonSchema, components) const responseSchema = jsonSchemaToOpenAPISchema(returns) responses = { 200: { description: 'Success', content: { 'application/json': { schema: responseSchema } } } } } } // --------------------------------------------------------------------------- // POST Operation // --------------------------------------------------------------------------- if (service.httpMethod === 'post') { const post = { requestBody, responses } if (definition.description) { post.description = definition.description } result.post = post } // --------------------------------------------------------------------------- // GET Operation // --------------------------------------------------------------------------- if (service.httpMethod === 'get') { const parameters = [] for (const [name, schema] of Object.entries(requestSchema.properties)) { const param = { name, schema, in: 'query' } if (schema.description) { param.description = schema.description } if (schema.$ref) { param.schema = { ...schema, definitions: requestSchema.definitions } } if (requestSchema.required && requestSchema.required.indexOf(name) >= 0) { param.required = true } parameters.push(param) } const get = { parameters, responses } if (definition.description) { get.description = definition.description } result.get = get } return { [route]: result } }