saasify-to-openapi
Version:
Converts Saasify deployments to OpenAPI specs.
274 lines (229 loc) • 6.66 kB
JavaScript
// 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
}
}