@axway/api-builder-runtime
Version:
API Builder Runtime
952 lines (884 loc) • 27.1 kB
JavaScript
const _ = require('lodash');
const chalk = require('chalk');
const util = require('util');
const Swagger = require('@axway/api-builder-openapi-doc');
const schemas = require('@axway/api-builder-schema');
const apiBuilderConfig = require('@axway/api-builder-config');
const ObjectModel = require('./objectmodel');
const yaml = require('js-yaml');
// For parsing npm author strings
// For example, "Barney Rubble <b@rubble.com> (http://barnyrubble.tumblr.com/)"
const authorRegex = /^(.*?)(?:\s+<(.*?)>)?(?:\s+\((.*?)\))?$/;
const verbMap = {
POST: 'create',
GET: 'find',
PUT: 'update',
DELETE: 'delete'
};
const ResponseModel = {
type: 'object',
required: [
'success',
'request-id'
],
properties: {
code: {
type: 'integer',
format: 'int32'
},
success: {
type: 'boolean'
},
'request-id': {
type: 'string'
},
message: {
type: 'string'
},
url: {
type: 'string'
}
}
};
const ErrorModel = {
type: 'object',
required: [
'message',
'code',
'success',
'request-id'
],
properties: {
code: {
type: 'integer',
format: 'int32'
},
success: {
type: 'boolean',
default: false
},
'request-id': {
type: 'string'
},
message: {
type: 'string'
},
url: {
type: 'string'
}
}
};
const UnauthorizedError = {
type: 'object',
required: [
'message',
'success',
'id'
],
properties: {
success: {
type: 'boolean'
},
id: {
enum: [
'com.appcelerator.api.unauthorized'
]
},
message: {
type: 'string'
}
},
additionalProperties: false
};
const PayloadTooLargeError = {
type: 'object',
required: [
'message',
'code',
'success',
'request-id'
],
properties: {
code: {
type: 'integer',
format: 'int32'
},
success: {
type: 'boolean'
},
'request-id': {
type: 'string'
},
message: {
type: 'string'
}
},
additionalProperties: false
};
/**
* Default response codes applied on the endpoints
*/
const emptySuccessResponse = {
description: 'The operation succeeded',
schema: {}
};
const responseByCode = {
201: emptySuccessResponse,
204: emptySuccessResponse,
401: {
description: 'Unauthorized',
schema: {
$ref: '#/definitions/UnauthorizedError'
}
},
413: {
description: 'Payload too large',
schema: {
$ref: '#/definitions/PayloadTooLargeError'
}
}
};
module.exports.bindRoutes = bindRoutes;
module.exports.generateSwagger = generateSwagger;
module.exports.schemaIdToSwaggerName = schemaIdToSwaggerName;
/**
* Converts a schema id to a swagger friendly name. Conversion as per the ticket RDPP-1673
*
* @param {string} id - Schema identifier, e.g. schema:///foo
* @returns {string} the friendly name
*/
function schemaIdToSwaggerName(id) {
if (id.startsWith('schema:///')) {
const ids = id.slice(10).split('/');
// e.g. schema:///model/mongo%2Fmovies-full.
// e.g. schema:///schema/demo/error
// The model name is URI encoded and should be decoded before adding to swagger
let name = decodeURIComponent(ids.slice(1).join('_'));
if (ids[0] && ids[0] !== 'model') {
name = ids[0] + '.' + name;
}
return name;
}
return id;
}
function bindRoutes(apibuilder) {
const app = apibuilder.app;
const objectModel = new ObjectModel(apibuilder);
const { getAbsoluteURLs } = apibuilder._internal;
const prefix = apibuilder.config.apidoc.prefix;
apibuilder.logger.trace('Registering swagger routes under path ' + prefix);
app.get(prefix + '/docs.json',
util.deprecate(getDefinition.bind(null, 'json'),
`${prefix}/docs.json is deprecated (${prefix}/swagger.json should be used instead). See: https://docs.axway.com/bundle/api-builder/page/docs/deprecations/index.html#D001`,
'D001'
)
);
app.get(prefix + '/swagger.json', getDefinition.bind(null, 'json'));
app.get(prefix + '/swagger.yml', getDefinition.bind(null, 'yaml'));
app.get(prefix + '/swagger.yaml', getDefinition.bind(null, 'yaml'));
// Get and log absolute URLs to the document for HTTP/HTTPs.
const docUrls = getAbsoluteURLs(`${prefix}/swagger.json`);
for (const url of docUrls) {
apibuilder.logger.info(
'Access the OpenAPI document generated for your service at',
chalk.yellow.underline(url));
}
// Generate the document once for local use. The only difference from the accessed
const generated = generateSwagger(
apibuilder,
// host. prefer http, fall back to https
apibuilder._internal.host || apibuilder._internal.sslHost,
objectModel,
// name unused
undefined,
// force unused
undefined,
// secure: if not running on HTTP then this is true and the scheme becomes HTTPS
apibuilder.host === undefined
);
apibuilder._internal.setDynamicOpenAPI(docUrls[0], generated);
function getDefinition(format, req, res) {
try {
var keys = Object.keys(req.query),
typeKeys = keys.filter(function (a) {
return a.startsWith('endpoints/') || a.startsWith('apis/');
}),
force = req.query.force ? (req.query.force === 'true' || req.query.force === '1')
: false,
ignoreOverrides = req.query.ignoreOverrides
? (req.query.ignoreOverrides === 'true' || req.query.ignoreOverrides === '1')
: false,
host = req.get('host'),
name,
type,
result;
// extract name and type. eg. ?apis/appc.arrowdb/acl (type=apis, name=appc.arrowdb/acl)
if (typeKeys.length) {
type = typeKeys[0].split('/')[0];
name = typeKeys[0]
.substr(type.length + 1, typeKeys[0].length).replace(/\.(json|html)/g, '');
}
result = generateSwagger(apibuilder, host, objectModel, type, name,
force, req.secure, ignoreOverrides);
if (format === 'yaml') {
result = yaml.safeDump(result);
res.set('Content-Type', 'text/yaml');
res.send(result);
} else if (typeof result === 'number') {
res.sendStatus(result);
} else {
res.set('Content-Type', 'application/json');
res.send(result);
}
} catch (ex) {
apibuilder.logger.error('Error generating swagger:', ex);
res.status(500).send({ error: 'Server error' });
}
}
}
/**
* Get the models referenced by fields in model composition.
*/
function getCompositionModels(om, modelName, models) {
var model = om.models[modelName];
if (model) {
var referencedModels = new Set(
Object.keys(model.fields)
.map(function (fieldName) {
return model.fields[fieldName];
})
.filter(function (field) {
return field.model && (
field.type === 'array'
|| field.type === Array
|| field.type === 'object'
|| field.type === Object);
})
.map(function (field) {
return field.model;
})
.filter(function (modelName) {
return !models || !models.has(modelName);
})
);
// Add the referenced models
models = models || new Set();
referencedModels.forEach(function (m) {
models.add(m);
});
// Look for nested dependencies
referencedModels.forEach(function (m) {
getCompositionModels(om, m, models);
});
}
return models;
}
// Get the models reference by the APIs
function getAPIModels(om) {
const referenceModels = new Set();
// Get a unique list of models reference by the API endpoints.
for (const apiName in om.apis) {
const api = om.apis[apiName];
referenceModels.add(apiName);
for (const endpoint of api.endpoints || []) {
if (endpoint.model) {
referenceModels.add(endpoint.model);
}
if (endpoint.response) {
referenceModels.add(endpoint.response);
}
}
}
// Extend the list to include models referenced via field composition
var referenceAndCompositionlModels = new Set(Array.from(referenceModels).map(
function (m) {
return transformKeyForComparison(m);
}
));
referenceModels.forEach(
function (model) {
var compModel = getCompositionModels(om, model);
compModel && compModel.forEach(function (compModel) {
return referenceAndCompositionlModels.add(transformKeyForComparison(compModel));
});
}
);
return _.pickBy(om.models, function (value, name) {
return referenceAndCompositionlModels.has(transformKeyForComparison(name));
});
}
function hasAPIPrefixSecurity(apibuilder) {
const { apiPrefixSecurity } = apibuilder.config.accessControl;
return apiPrefixSecurity !== 'none';
}
function addAPIPrefixSecurity(apibuilder, swagger) {
if (!hasAPIPrefixSecurity(apibuilder)) {
return;
}
swagger.schema('UnauthorizedError', UnauthorizedError);
}
/**
* Returns true if any limit has been configured and enabled that would
* result in the service returning 413
*/
function hasPayloadLimit(apibuilder) {
// We have one limit right now but this could expand to different limits.
const { multipartPartSize } = apibuilder.config.limits;
// Limit is only going to possibly fire if multipartPartSize is finite
return multipartPartSize !== Infinity;
}
function addPayloadTooLarge(apibuilder, swagger) {
if (!hasPayloadLimit(apibuilder)) {
return;
}
swagger.schema('PayloadTooLargeError', PayloadTooLargeError);
}
/**
* Generates a swagger document for the current api builder instance
* @param {object} apibuilder api builder instance
* @param {string} host host that is requesting the doc
* @param {object} objectModel api builder object model instance
* @param {string} type either 'apis' or 'endpoints' when fetching swagger by name
* @param {string} name the name of the api/endpoint to generate swagger for
* @param {bool} force include disabled endpoints in the requested swagger - does not
* effect requests for a specific endpoint/api
* @param {bool} secure is the server running ssl?
* @param {bool} ignoreOverrides do not set user provided overrides on the swagger document
*/
function generateSwagger(
apibuilder, host, objectModel, type, name, force, secure, ignoreOverrides) {
const swagger = new Swagger();
let om;
const docOverrides = ignoreOverrides ? {} : apibuilder.config.apidoc.overrides;
// use overriden host or host from request, or fall back to localhost and server port.
const port = secure ? apibuilder.config.ssl.port : apibuilder.port;
const myHost = docOverrides.host !== undefined
? docOverrides.host : host || `127.0.0.1:${port}`;
const basePath = docOverrides.basePath !== undefined
? docOverrides.basePath : apibuilder.config.apiPrefix;
// default to http, use https if accessed over ssl or use the override
const mySchemes = docOverrides.schemes !== undefined
? docOverrides.schemes : [ secure ? 'https' : 'http' ];
// Set the swagger API info
swagger.info(
apibuilder.metadata.name || 'API',
apibuilder.metadata.version || '1.0',
apibuilder.metadata.description || 'API description'
);
if (myHost) {
swagger.host(myHost);
}
if (basePath) {
swagger.basePath(basePath);
}
/* Applying globalSchemes here has no affect because there are no paths in the
* initial state. The merge will first copy all global schemes to the current
* local paths, and delete the globals. But since there are no paths, the globals
* are deleted. So, the merge will have the affect of removing schemes from the
* document. This is works as designed, just a little unexpected. It is more
* correct to not have schemes in the Swagger - they default to the scheme used
* to access the document. In fact, schemes do not detail how to access each scheme.
*/
// .globalSchemes(createSchemes(objectModel));
/* Attempts to parse the contact details. Supports:
* 1. Author information being a string:
* "Barney Rubble <b@rubble.com> (http://barnyrubble.tumblr.com/)
* 2. Author information being an object - containing name, email(optional)
* and url(optional):
* "author": {
* "email": "banana@axway.com",
* "name": "Banana",
* "url": "https://axway.com"
* }
*/
if (typeof apibuilder.metadata.author === 'object') {
const { name, email, url } = apibuilder.metadata.author;
swagger.contact(name, email, url);
} else if (apibuilder.metadata.author) {
const match = authorRegex.exec(apibuilder.metadata.author);
if (match) {
const [ , name, email, url ] = match;
swagger.contact(name, email, url);
}
}
apibuilder.metadata.license && swagger.license(apibuilder.metadata.license);
// return 404 for undefined name or invalid types
if (type && (!name || [ 'apis', 'endpoints' ].indexOf(type) === -1)) {
return 404;
}
// Merge in the valid enabled endpoints
if (!type || type === 'endpoints') {
const endpoints = apibuilder && apibuilder.endpoints;
if (endpoints) {
if (type && !endpoints[name]) {
return 404;
}
function filter(swagger, path, verb) {
if (force) {
return true;
}
const xenabled = swagger.paths[path][verb]['x-enabled'];
if (xenabled && xenabled.enabled === false) {
return false;
}
return true;
}
Object.keys(endpoints).forEach(function (epName) {
const xEnabled = endpoints[epName].endpoint['x-enabled'];
const endpointSwagger = endpoints[epName].swagger;
let mergeOptions;
if (!type && xEnabled && xEnabled.enabled) {
// in case of a consolidated swagger, merge endpoints on their basepath to avoid
// collision. also need to filter out the disabled endpoint paths
mergeOptions = {
prefix: endpointSwagger.basePath,
filter: filter,
extensions: /^x-(?!(enabled|flow)$)/,
mergeBlacklist: [ 'schemes', 'securityDefinitions' ],
// swagger merge option `express` is to *never* format `:name` to express
// as the endpoint is already Swagger
express: false
};
} else if (epName === name) {
swagger.basePath(basePath + (endpointSwagger.basePath || ''));
mergeOptions = {
filter: filter,
extensions: /^x-(?!(enabled|flow)$)/,
mergeBlacklist: [ 'schemes', 'securityDefinitions' ],
// swagger merge option `express` is to *never* format `:name` to express
// as the endpoint is already Swagger
express: false
};
} else {
// not a valid/enabled endpoint or not the specific named
// endpoint we're looking for.
return;
}
if (hasAPIPrefixSecurity(apibuilder)) {
// this is providing the 401 response which is to be
// applied to operation responses when merging.
mergeOptions.responses = {
401: responseByCode[401]
};
}
if (hasPayloadLimit(apibuilder)) {
// We add 413 to all methods since we can't always guarantee which APIs
// will accept an upload. This was the simplest solution.
mergeOptions.responses = mergeOptions.responses || {};
mergeOptions.responses[413] = responseByCode[413];
}
swagger.merge(endpointSwagger, mergeOptions);
});
}
}
// Merge in the model endpoints
var lookupDefinitions = {};
if (!type || type === 'apis') {
if (type) {
om = _.clone(objectModel);
var transQuery = transformKeyForComparison(name); // lower-case
function matchesQuery(value, key) {
return transformKeyForComparison(key) === transQuery;
}
om.apis = _.pickBy(om.apis, matchesQuery);
if (!Object.keys(om.apis).length) {
return 404;
}
om.models = getAPIModels(om);
} else {
om = objectModel;
}
if (om && om.apis && Object.keys(om.apis).length) {
swagger.schemas(createDefinitions(apibuilder, om, lookupDefinitions));
swagger.paths(createPaths(om, apibuilder, lookupDefinitions));
}
}
// Adding UnauthorizedError to schemas if we have security enabled
addAPIPrefixSecurity(apibuilder, swagger);
// Add payload too large error to schemas if we have limits in place
addPayloadTooLarge(apibuilder, swagger);
// Security definition (see: apibuilder/lib/authentication/index.js)
// The getSwaggerSecurity method is optional. If the auth plugin defines one, then it
// should be used.
const sec = apibuilder.authStrategy.getSwaggerSecurity();
if (sec && sec.securityDefinitions) {
const defs = sec.securityDefinitions;
if (defs) {
Object.keys(defs).forEach(function (key) {
swagger.securityDefinition(key, defs[key]);
});
}
if (sec.security) {
if (sec.security instanceof Array) {
swagger.globalSecurity(sec.security);
} else if (apibuilder.logger) {
apibuilder.logger.error('invalid swagger security definition: ', sec.security);
}
}
}
var doc = swagger.apidoc();
// Define the scheme used to access the document
if (mySchemes) {
doc.schemes = mySchemes;
}
// dereference all $ref to loaded schemas
return schemas.dereference(doc, {
target: '#/definitions',
rename: schemaIdToSwaggerName
});
}
function transformKeyForComparison(val) {
return val.replace(/[^a-z0-9]/ig, '').toLowerCase();
}
function createDefinitions(apibuilder, objectModel, lookupDefinitions) {
const retVal = {};
const models = objectModel.models;
Object.entries(models).forEach(([ modelName, model ]) => {
const schema = [
{
key: `#/definitions/${modelName.replace(/\//, '_')}`,
id: apibuilder.getModelSchemaId(model)
},
{
key: `#/definitions/${modelName.replace(/\//, '_')}-ex`,
id: apibuilder.getModelSchemaExId(model)
}
];
if (!apiBuilderConfig.flags.enableModelsWithNoPrimaryKey
|| !(model.metadata && model.metadata.primarykey === null)) {
schema.push(
{
key: `#/definitions/${modelName.replace(/\//, '_')}-full`,
id: apibuilder.getModelSchemaFullId(model)
},
{
key: `#/definitions/${modelName.replace(/\//, '_')}-fullEx`,
id: apibuilder.getModelSchemaFullExId(model)
}
);
}
schema.forEach(({ key, id }) => {
if (!schemas.get(id)) {
let msg = `failed to get schema for model: ${modelName}`;
apibuilder.logger.error(msg);
throw new Error(msg);
}
lookupDefinitions[key] = id;
});
});
retVal.ResponseModel = ResponseModel;
retVal.ErrorModel = ErrorModel;
return retVal;
}
// This is precalculated in the api constructor based on the
// model/response or predefined value
function getResponseKey(endpoint, isArray) {
if (isArray) {
return endpoint.plural || 'result';
} else {
return endpoint.singular || 'result';
}
}
function generateResponses(apibuilder, endpoint, lookupDefinitions) {
const isEmpty = !endpoint.responses || !Object.keys(endpoint.responses).length;
const responses = isEmpty
? generateResponseByModel(endpoint, lookupDefinitions)
: generateResponsesByStatus(endpoint, lookupDefinitions);
if (hasAPIPrefixSecurity(apibuilder)) {
responses[401] = responseByCode[401];
} else if (endpoint.generated) {
// When security is disabled, only delete the 401 responses
// for model auto-generated API. If custom API have 401
// responses, these are OK.
delete responses[401];
}
if (hasPayloadLimit(apibuilder)) {
// 413 gets added to all APIs since we can't guarantee which ones actually
// use/accept a multipart file/field
responses[413] = responseByCode[413];
}
return responses;
}
// This is specific for defining outputs for custom APIs. It leverage
// 'response' and 'responseIsArray' properties.
function generateResponseByModel(endpoint, lookupDefinitions) {
const response = endpoint.response;
const responseSpec = {};
// The user can configure if their API response is going to be documented
// as returning an array or not. It defaults to false.
let isArray = endpoint.responseIsArray;
let ref = {};
// Get the response key that was set by the API. This can be a custom
// singular/plural, or the one from the response model, otherwise
// falling back to 'result'.
const key = getResponseKey(endpoint, isArray);
if (response) {
ref.$ref = lookupDefinitions[
`#/definitions/${response.replace(/\//, '_')}-full`
];
}
if (isArray) {
ref = {
type: 'array',
items: ref
};
}
// Use default to apply generically to any possible response code.
responseSpec.default = {
description: 'Response from server',
schema: {
allOf: [
{ $ref: '#/definitions/ResponseModel' },
{
type: 'object',
properties: {
key: {
type: 'string',
enum: [ key ]
},
[key]: ref
}
}
]
}
};
const method = endpoint.method.toLowerCase();
if ([ 'put', 'delete' ].includes(method)) {
responseSpec[204] = responseByCode[204];
} else if (method === 'post') {
responseSpec[201] = responseByCode[201];
}
return responseSpec;
}
// Add responses in the api doc when specified. The 'responses' are
// usually available in the generated APIs. They are unlikely to be in
// set on custom APIs because 'responses' is not documented.
// Still, we will process 'responses' for all APIs as an
// advanced mechanism to allow the user to override model/response
// properties.
function generateResponsesByStatus(endpoint, lookupDefinitions) {
const responses = endpoint.responses;
const responseSpec = {};
for (const status in responses) {
const response = responses[status];
if (endpoint.generated && status === '200') {
// TODO: move this to the autogenerated API code and
// make use of allOf just like generateResponsesByModel
const schema = JSON.parse(JSON.stringify(ResponseModel));
if (response.schema) {
const isArray = response.schema.type === 'array';
const key = getResponseKey(endpoint, isArray);
schema.required.push('key');
schema.properties.key = {
type: 'string',
enum: [ key ]
};
translateSchemaRefs(response, lookupDefinitions);
schema.properties[key]
= JSON.parse(JSON.stringify(response.schema));
delete schema.properties[key].description;
}
responseSpec[status] = {
description: response.description
|| `${endpoint.name} Response`,
schema,
headers: responses.headers,
examples: responses.examples
};
} else {
// Only wrapping success
responseSpec[status] = response;
}
}
return responseSpec;
}
/**
* Lookup the model definition and return the schema:/// equivalent, properly
* encoded. The effect it has:
* - swaps $refs to #/definitions/ with schema:///
* - if the model contains slash e.g. "mongo/lime" the '/' is replaced with '_'
* - the refs will contain the encoded version of '_' which is '~1'
*/
function translateSchemaRefs(response, lookupDefinitions) {
const { schema } = response;
const { items } = schema;
if (schema.$ref && schema.$ref in lookupDefinitions) {
schema.$ref = lookupDefinitions[schema.$ref];
} else if (items && items.$ref && items.$ref in lookupDefinitions) {
items.$ref = lookupDefinitions[items.$ref];
}
}
function createPaths(objectModel, apiBuilder, lookupDefinitions) {
const paths = {};
const apis = objectModel.apis;
for (const groupName in apis) {
if (apis.hasOwnProperty(groupName)) {
const api = apis[groupName];
for (const endpoint of api.endpoints) {
// These are not swagger "Endpoints". These are still "APIs"
const eppath = endpoint.pathUnescaped || endpoint.path;
let newRelativePath = eppath;
if (eppath.startsWith(apiBuilder.config.apiPrefix)) {
newRelativePath = eppath.replace(apiBuilder.config.apiPrefix, '');
}
const relativePath = translatePath(newRelativePath);
let def = paths[relativePath];
if (endpoint.enabled === false) {
continue;
}
if (!def) {
paths[relativePath] = def = {};
}
const pathID = endpoint.method.toLowerCase();
const addConsumes = [ 'post', 'put', 'patch', 'options' ].includes(pathID);
def[pathID] = compact({
summary: endpoint.description,
operationId: getOperationId(endpoint),
deprecated: endpoint.deprecated,
parameters: translateParameters(endpoint, apiBuilder.logger),
responses: generateResponses(
apiBuilder,
endpoint,
lookupDefinitions
),
tags: [ groupName ],
produces: [
'application/json',
'application/xml',
'text/yaml',
'text/csv',
'text/plain'
],
...addConsumes ? {
consumes: [
'application/json',
'application/x-www-form-urlencoded',
'multipart/form-data'
]
} : {}
});
}
}
}
return {
paths: paths
};
}
function translatePath(path) {
return path.replace(/:([^/]+)\?/g, '{$1}');
}
function translateParameters(endpoint, logger) {
const retVal = [];
let bodyParams;
for (const name in endpoint.parameters) {
if (!endpoint.parameters.hasOwnProperty(name)) {
continue;
}
const param = endpoint.parameters[name];
if (param.type === 'body') {
if (!bodyParams) {
bodyParams = {
name: endpoint.nickname,
in: 'body',
description: endpoint.nickname + ' body',
required: true,
schema: {
type: 'object',
required: [],
properties: {}
}
};
}
if (param.required) {
bodyParams.schema.required.push(name);
}
bodyParams.schema.properties[name] = transformAPIBuilderProperty(param);
continue;
}
if (param.type === 'form' && bodyParams) {
// We can't define both body and form params; there can be only one body per endpoint.
if (param.required) {
bodyParams.schema.required.push(name);
}
bodyParams.schema.properties[name] = transformAPIBuilderProperty(param);
continue;
}
// Force form parameters to be required (as required by the Swagger spec).
if (param.type === 'path' && !param.required) {
logger.warn(`OpenAPI does not support optional path parameters. Parameter ${name} for ${endpoint.method} ${endpoint.path} will be documented as required`);
param.required = true;
}
const translated = {
name: name,
in: param.type,
description: param.description,
required: !!param.required,
type: param.dataType || 'string'
};
// TODO: We need more information about the sub-types of objects and arrays.
if (param.dataType === 'object') {
translated.type = 'string';
}
if (param.dataType === 'array') {
translated.items = { type: 'object' };
}
if (param.dataType === 'date') {
translated.type = 'string';
}
retVal.push(compact(translated));
}
if (bodyParams) {
if (bodyParams.schema.required.length === 0) {
delete bodyParams.schema.required;
}
retVal.push(bodyParams);
}
return retVal;
}
function transformAPIBuilderProperty(apibuilderProperty) {
var dataType = apibuilderProperty.dataType || 'string';
var swaggerProperty = {
type: dataType,
description: apibuilderProperty.description
};
switch (dataType) {
case 'date':
swaggerProperty.type = 'string';
break;
case 'array': {
swaggerProperty.items = { type: 'string' };
break;
}
}
return swaggerProperty;
}
function getOperationId(endpoint) {
let retVal = verbMap[endpoint.method] || endpoint.method.toLowerCase();
const splits = endpoint.path.replace(/_[a-z]/ig, (val) => {
return val[1].toUpperCase();
})
.slice(1).split('/');
for (var i = 1; i < splits.length; i++) {
var split = splits[i];
if (split[0] === ':') {
retVal += 'By' + split.slice(1).toUpperCase();
} else if (split[0] !== undefined) {
retVal += split[0].toUpperCase() + split.slice(1);
}
}
return retVal;
}
function compact(obj) {
return _.omitBy(obj, function (val, key) {
if (Array.isArray(val)) {
return val.length === 0;
}
if (_.isObject(val)) {
obj[key] = compact(val);
if (Object.keys(obj[key]).length === 0) {
return false;
}
}
return !val;
});
}